本文 AI 產出,尚未審核

FastAPI 教學:Session 與 Cookie 管理 – SessionMiddleware 中間層


簡介

在 Web 應用程式中,使用者狀態(例如登入資訊、購物車內容、偏好設定)往往需要跨多個請求持續保存。若沒有適當的機制,前端每次發送請求都必須重新驗證,既浪費資源又影響使用者體驗。Session(會話)與 Cookie 正是解決這類需求的核心工具。

FastAPI 雖然本身是以「無狀態」的 API 為設計哲學,但在實務開發中,我們仍會需要 SessionMiddleware 來管理使用者的會話資訊。本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你掌握 FastAPI 中的 Session 中間層。


核心概念

1. Session 與 Cookie 的關係

  • Cookie:瀏覽器端的鍵值對,會在每次 HTTP 請求時自動帶上。常用來存放 Session ID、認證 token、或是簡單的偏好設定。
  • Session:伺服器端的資料結構,根據 Session ID(通常由 Cookie 提供)去查找對應的使用者資訊。Session 的內容不會直接暴露給客戶端,安全性較高。

簡單比喻:Cookie 就像是門口的門票號碼,Session 則是門票號碼背後的「座位卡」—只有持有正確號碼才能取得座位資訊。

2. 為什麼需要 SessionMiddleware

FastAPI 本身只提供路由與依賴注入,沒有內建的 Session 管理。SessionMiddlewareStarlette(FastAPI 的底層框架)提供的中間層,負責:

  1. 讀取/寫入 Cookie 中的 Session ID
  2. request.state.session 上掛載一個可變的字典,讓路由函式直接存取會話資料
  3. 在回應階段自動把更新過的 Session 資料寫回 Cookie(或其他儲存後端)

使用 SessionMiddleware 後,我們可以像操作普通字典一樣管理會話,免除自行編寫 Cookie 解析與加密的繁雜工作。

3. Session 的儲存方式

SessionMiddleware 預設使用 簽名的 Cookiesigned)直接在客戶端保存會話資料,適合小量、非機密的資訊。若需要更安全或更大的儲存空間,常見的做法是:

  • 伺服器端儲存:將 Session ID 存於 Cookie,真正的會話資料放在 Redis、資料庫或檔案系統。
  • 加密 Cookie:使用 itsdangerouscryptography 把資料加密後寫入 Cookie。

本文的範例會先示範最簡單的 簽名 Cookie,再說明如何結合 Redis 進行伺服器端儲存。


程式碼範例

以下範例全部使用 Python(FastAPI/Starlette),並以 ````python` 標記。

範例 1️⃣ 基本的 SessionMiddleware(簽名 Cookie)

from fastapi import FastAPI, Request, Response
from starlette.middleware.sessions import SessionMiddleware

app = FastAPI()

# 設定密鑰(必須長且隨機),用於簽名 Cookie
app.add_middleware(SessionMiddleware, secret_key="YOUR_SUPER_SECRET_KEY")

@app.get("/set")
def set_session(request: Request):
    # 在 session 中寫入資料
    request.session["username"] = "alice"
    request.session["counter"] = request.session.get("counter", 0) + 1
    return {"msg": "Session 已設定", "session": request.session}

@app.get("/get")
def get_session(request: Request):
    # 直接讀取 session
    username = request.session.get("username")
    counter = request.session.get("counter", 0)
    return {"username": username, "counter": counter}

說明

  • SessionMiddleware 會在每個請求的 request.session 上掛載一個 dict‑like 物件。
  • secret_key 必須保密,否則惡意使用者可以自行偽造 Cookie。
  • 只要在路由裡修改 request.session,回應時中間層會自動把變更寫回 Cookie。

範例 2️⃣ 使用 Redis 作為 Session 後端

前置作業pip install redis fastapi[all],並確保本機或遠端有 Redis 服務。

import uuid
import json
import redis
from fastapi import FastAPI, Request, Response, Depends
from starlette.middleware.base import BaseHTTPMiddleware

# 建立 Redis 連線(可使用環境變數或設定檔)
redis_client = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)

class RedisSessionMiddleware(BaseHTTPMiddleware):
    """
    自訂 Middleware:把 Session ID 放在 Cookie,
    真正的資料存於 Redis。
    """
    def __init__(self, app, secret_key: str, cookie_name: str = "session_id"):
        super().__init__(app)
        self.secret_key = secret_key
        self.cookie_name = cookie_name

    async def dispatch(self, request: Request, call_next):
        # 1. 取得或產生 session_id
        session_id = request.cookies.get(self.cookie_name)
        if not session_id:
            session_id = str(uuid.uuid4())
            # 新增空的 session
            redis_client.set(session_id, json.dumps({}))
        # 2. 把 session 資料掛載到 request.state
        raw = redis_client.get(session_id) or "{}"
        request.state.session = json.loads(raw)
        request.state.session_id = session_id

        # 3. 處理請求
        response: Response = await call_next(request)

        # 4. 把變更寫回 Redis
        redis_client.set(session_id, json.dumps(request.state.session))

        # 5. 設定 Cookie(HttpOnly 提升安全性)
        response.set_cookie(
            key=self.cookie_name,
            value=session_id,
            httponly=True,
            max_age=60 * 60 * 24 * 7,  # 7 天
        )
        return response

app = FastAPI()
app.add_middleware(RedisSessionMiddleware, secret_key="another_secret_key")

def get_session(request: Request) -> dict:
    """依賴注入,取得 session dict"""
    return request.state.session

@app.post("/login")
def login(username: str, request: Request):
    # 假設驗證成功,寫入 session
    request.state.session["user"] = username
    return {"msg": f"{username} 已登入"}

@app.get("/profile")
def profile(session: dict = Depends(get_session)):
    user = session.get("user")
    if not user:
        return {"error": "未登入"}
    return {"user": user, "info": "這是使用者個人資料"}

說明

  1. Session ID 只是一個 UUID,存於 Cookie 中;真正的資料放在 Redis。
  2. request.state.session 為 Python dict,可在任何路由或依賴中直接讀寫。
  3. HttpOnlySecure(若使用 HTTPS)可提升 Cookie 的安全性。

範例 3️⃣ 加密 Cookie(使用 itsdangerous

from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from itsdangerous import URLSafeSerializer, BadSignature

app = FastAPI()
serializer = URLSafeSerializer("ENCRYPTION_SECRET_KEY")

class EncryptedCookieMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 讀取 Cookie 並解密
        raw = request.cookies.get("enc_session")
        if raw:
            try:
                data = serializer.loads(raw)
            except BadSignature:
                data = {}
        else:
            data = {}
        request.state.session = data

        response: Response = await call_next(request)

        # 把更新後的資料重新加密寫回 Cookie
        encrypted = serializer.dumps(request.state.session)
        response.set_cookie(
            key="enc_session",
            value=encrypted,
            httponly=True,
            max_age=60 * 60 * 24,
        )
        return response

app.add_middleware(EncryptedCookieMiddleware)

@app.get("/set_enc")
def set_enc(request: Request):
    request.state.session["token"] = "abc123"
    return {"msg": "已加密寫入 session"}

@app.get("/read_enc")
def read_enc(request: Request):
    return {"session": request.state.session}

說明

  • itsdangerous.URLSafeSerializer 會把 Python dict 序列化為 URL‑safe 的字串,同時加入簽名,防止被竄改。
  • 若收到的 Cookie 無法解密或簽名不符,會回傳空的 session,避免程式崩潰。

範例 4️⃣ 透過依賴注入共享 Session(中階寫法)

from fastapi import FastAPI, Depends, Request

app = FastAPI()

def get_current_user(request: Request):
    """
    依賴函式:從 session 取得目前使用者名稱,若不存在則拋出例外
    """
    user = request.session.get("username")
    if not user:
        raise HTTPException(status_code=401, detail="未登入")
    return user

@app.get("/dashboard")
def dashboard(user: str = Depends(get_current_user)):
    return {"msg": f"歡迎 {user} 進入儀表板"}

說明

  • 只要在 SessionMiddleware 已經掛載 request.session,就能在任何依賴函式中直接存取。
  • 這樣的寫法讓 認證/授權 的邏輯集中管理,維護性更佳。

範例 5️⃣ Session 失效與手動清除

@app.post("/logout")
def logout(request: Request, response: Response):
    # 清除 server 端資料(若使用 Redis)
    session_id = request.cookies.get("session_id")
    if session_id:
        redis_client.delete(session_id)

    # 刪除 Cookie(設定過期時間為過去)
    response.delete_cookie("session_id")
    return {"msg": "已登出,Session 已清除"}

說明

  • response.delete_cookie 會在 Set‑Cookie 標頭中寫入 Expires=Thu, 01 Jan 1970 00:00:00 GMT,讓瀏覽器立即刪除。
  • 若 Session 存於 Redis,別忘了同步刪除對應的 key,避免資源浪費。

常見陷阱與最佳實踐

陷阱 可能的後果 解決方案 / 最佳實踐
密鑰硬編碼 失竊後所有 Cookie 可被偽造 使用環境變數或 secret manager,且定期輪換
過大的 Cookie 超過瀏覽器限制(約 4KB)導致無法寫入 僅存放 Session ID,真正資料放在伺服器端(Redis、DB)
未設定 HttpOnly / Secure JavaScript 可讀取 Cookie,易受 XSS 攻擊 HttpOnly=True 防止 JS 存取;在 HTTPS 時加上 Secure=True
Session 過期未處理 使用者持續使用舊的 Session,造成資安風險 設定合理的 max_age,並在每次請求時檢查過期時間
同時使用多個 Session 中間層 互相覆寫、混淆 確保只掛載一次 SessionMiddleware,或自行設計名稱空間(不同 cookie 名稱)
在異步函式中直接修改 request.session 競爭條件(同一 Session 被多個請求同時寫) 若使用 Redis,建議使用 HMSET 或 Lua script 進行原子寫入

其他最佳實踐

  1. 最小化 Session 資料:只存放必要的鍵值(例如 user_id),其他資訊可在需要時再查 DB。
  2. 使用類型安全的 Session 介面:可自行封裝 SessionDict,限制只能存放 JSON‑serializable 物件。
  3. 監控與日誌:記錄 Session 建立、失效、異常簽名等事件,方便安全審計。
  4. 分離測試環境:測試時使用 TestClient 並自行模擬 Cookie,避免在 CI 中依賴外部 Redis。

實際應用場景

場景 為何需要 Session 建議的實作方式
使用者登入認證 保存 user_id、JWT 失效時間等 SessionMiddleware + Redis(高併發)
購物車 多頁面、跨請求的商品暫存 簽名 Cookie(商品 ID、數量)或 Redis(大量商品)
多語系/主題偏好 客製化 UI 需要在每次請求讀取 簽名 Cookie(小量文字)
API 節流/防止重複提交 記錄最近一次的請求 ID 簽名 Cookie 或內存快取(如 cachetools
SSO(單點登入) 需要在多個子系統間共享 Session 中央 Redis + 統一 Session ID,配合 JWT 交換

範例:在電商平台,使用者登入後的 Session 只存 user_id,購物車資料則寫入 Redis 的 cart:{user_id} 鍵。這樣即使使用者在不同裝置登入,仍能即時同步購物車。


總結

  • SessionMiddleware 為 FastAPI 提供了簡潔的會話管理入口,只要掛載一次,即可在 request.session(或 request.state.session)上直接讀寫字典資料。
  • 根據 安全性資料量併發需求,可以選擇 簽名 Cookie加密 Cookie伺服器端 Redis 等不同儲存策略。
  • 最佳實踐 包含:保護密鑰、設定 HttpOnly/Secure、限制 Cookie 大小、使用依賴注入統一授權邏輯、以及適時清除過期 Session。
  • 透過本篇提供的 5 個範例,你可以快速在專案中加入登入、購物車、偏好設定等常見功能,並在未來根據需求擴展至更高階的分布式 Session 解決方案。

掌握了 Session 中間層的概念與實作,你的 FastAPI 應用將不再是「無狀態」的孤島,而是能夠提供流暢、個人化的使用者體驗。祝開發順利,持續探索 FastAPI 的更多可能! 🚀