本文 AI 產出,尚未審核

FastAPI 課程 – Session 與 Cookie 管理

主題:Session ID 與 Server‑Side Session 儲存


簡介

在 Web 應用程式中,使用者的登入狀態、購物車內容或個人化設定 必須在多個請求之間保留。最常見的做法是透過 Cookie 搭配 Session ID,將唯一的辨識碼儲存在使用者端,並在伺服器端維護真正的資料。

FastAPI 本身是無狀態(stateless)的框架,若直接使用 Request.session 之類的功能會失去 FastAPI 輕量、非同步的特性。因此,我們需要自行設計或套用成熟的 server‑side session 解決方案,才能在保持效能的同時,提供安全、可擴充的 Session 管理。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步在 FastAPI 中完成 Session ID + Server‑Side Session 的完整流程,讓你的 API 能安全地記住使用者狀態。


核心概念

1. Session ID 與 Cookie 的關係

角色 位置 內容 目的
Session ID 只存於 Cookie(或 URL) 一串唯一的隨機字串(如 a3f9c2e5... 作為伺服器端 Session 資料的索引鍵
Cookie 使用者瀏覽器 Set-Cookie: session_id=...; HttpOnly; Secure; SameSite=Lax 把 Session ID 傳回伺服器,讓伺服器能找回對應的 Session 內容

重點:Session ID 不應直接包含使用者資料,所有敏感資訊必須存放在伺服器端。

2. 為什麼要使用 Server‑Side Session

  1. 安全性:即使 Cookie 被竊取,攻擊者只能取得 Session ID,若伺服器端實作了過期、IP/UA 檢查,仍能降低風險。
  2. 資料容量:Cookie 大小受限(約 4KB),而伺服器端可以儲存任意大小的資料(如購物車清單)。
  3. 集中管理:在分散式環境(多台 FastAPI 實例)下,只要使用共享的 Session 儲存(Redis、Memcached),即可保持一致性。

3. Session 的生命週期

  1. 產生:使用者第一次登入或訪問需要 Session 的路由,伺服器產生唯一的 Session ID,並將空的 Session 物件寫入儲存後端。
  2. 存取:每次請求攜帶 Cookie 時,FastAPI 透過 Session ID 從儲存後端讀取對應的 Session 資料。
  3. 更新:在請求處理完畢前,若有變更(如加入購物車),會把 Session 重新寫回儲存後端。
  4. 銷毀:使用者登出或 Session 超時時,刪除對應的 Session 並清除 Cookie。

程式碼範例

以下範例採用 Redis 作為 Session 後端,並使用 fastapiuvicornredis-py。若沒有 Redis 環境,可改用 MemoryStore(僅開發測試用)。

3.1 安裝必要套件

pip install fastapi uvicorn redis python-multipart

3.2 建立 Session 中介層(Middleware)

# session_middleware.py
import uuid
import json
from typing import Callable

from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
import redis

# ---------- Redis 連線 ----------
redis_client = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)

# ---------- Helper ----------
def generate_session_id() -> str:
    """產生唯一的 Session ID(UUID4)"""
    return str(uuid.uuid4())

def get_session_data(session_id: str) -> dict:
    """從 Redis 取得 Session 資料,若不存在回傳空 dict"""
    raw = redis_client.get(session_id)
    return json.loads(raw) if raw else {}

def save_session_data(session_id: str, data: dict, ttl: int = 1800):
    """寫入 Session,預設 30 分鐘過期"""
    redis_client.setex(session_id, ttl, json.dumps(data))

def delete_session(session_id: str):
    redis_client.delete(session_id)

# ---------- Middleware ----------
class SessionMiddleware(BaseHTTPMiddleware):
    """FastAPI Middleware,負責讀寫 Cookie 與 Redis Session"""

    def __init__(self, app, cookie_name: str = "session_id", max_age: int = 1800):
        super().__init__(app)
        self.cookie_name = cookie_name
        self.max_age = max_age  # 秒

    async def dispatch(self, request: Request, call_next: Callable):
        # 1️⃣ 讀取 Cookie 中的 session_id
        session_id = request.cookies.get(self.cookie_name)

        # 2️⃣ 若不存在,產生新的 Session ID
        if not session_id:
            session_id = generate_session_id()
            session_data = {}
        else:
            session_data = get_session_data(session_id)

        # 把 session_data 暴露給 downstream handler
        request.state.session = session_data
        request.state.session_id = session_id

        # 3️⃣ 呼叫實際路由
        response: Response = await call_next(request)

        # 4️⃣ 若 session 有變更,寫回 Redis
        if getattr(request.state, "session_modified", False):
            save_session_data(session_id, request.state.session, ttl=self.max_age)

        # 5️⃣ 設定 Cookie(若是新產生的或要延長有效期)
        response.set_cookie(
            key=self.cookie_name,
            value=session_id,
            max_age=self.max_age,
            httponly=True,
            secure=True,          # 僅在 HTTPS 下傳送
            samesite="lax",
        )
        return response

說明

  • request.state.session 為本次請求的 Session 物件,路由可以直接讀寫。
  • request.state.session_modified 必須在改變資料時手動設為 True,讓 Middleware 知道需要儲存。

3.3 建立 FastAPI 應用並掛載 Middleware

# main.py
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.responses import JSONResponse
from session_middleware import SessionMiddleware

app = FastAPI()
app.add_middleware(SessionMiddleware, max_age=1800)   # 30 分鐘

# ---------- 依賴注入取得 Session ----------
def get_session(request: Request):
    return request.state.session

def mark_modified(request: Request):
    request.state.session_modified = True

# ---------- 登入路由 ----------
@app.post("/login")
async def login(username: str, password: str, request: Request):
    # 這裡僅示範,實務請使用資料庫與 hash 驗證
    if username == "admin" and password == "secret":
        # 把使用者資訊寫入 Session
        request.state.session["user"] = {"username": username}
        request.state.session_modified = True
        return JSONResponse({"msg": "登入成功"})
    raise HTTPException(status_code=401, detail="帳號或密碼錯誤")

# ---------- 需要驗證的路由 ----------
def require_login(session: dict = Depends(get_session)):
    if "user" not in session:
        raise HTTPException(status_code=401, detail="未登入")
    return session["user"]

@app.get("/profile")
async def profile(user: dict = Depends(require_login)):
    return {"username": user["username"], "role": "admin"}

# ---------- 加入購物車 ----------
@app.post("/cart/add")
async def add_to_cart(item_id: int, request: Request, user: dict = Depends(require_login)):
    cart = request.state.session.get("cart", [])
    cart.append(item_id)
    request.state.session["cart"] = cart
    request.state.session_modified = True
    return {"msg": f"商品 {item_id} 已加入購物車", "cart": cart}

# ---------- 登出 ----------
@app.post("/logout")
async def logout(request: Request):
    session_id = request.state.session_id
    # 清除 Redis 中的 Session
    from session_middleware import delete_session
    delete_session(session_id)

    # 清除 Cookie
    response = JSONResponse({"msg": "已登出"})
    response.delete_cookie(key="session_id")
    return response

重點

  • require_login 透過 依賴注入(Dependency Injection)取得 Session,確保所有需要驗證的路由都能共用同一段檢查程式碼。
  • add_to_cart 示範 修改 Session 時必須手動設定 request.state.session_modified = True,否則 Middleware 不會寫回 Redis。

3.4 使用 MemoryStore(開發測試)

如果你沒有 Redis,可以把 session_middleware.py 中的 Redis 部分改為簡易的字典:

# 改寫為全域 dict(僅開發使用)
_memory_store = {}

def get_session_data(session_id: str) -> dict:
    return _memory_store.get(session_id, {})

def save_session_data(session_id: str, data: dict, ttl: int = 1800):
    _memory_store[session_id] = data
    # TTL 省略,開發階段不需要

def delete_session(session_id: str):
    _memory_store.pop(session_id, None)

3.5 完整測試腳本(使用 httpx)

# test_client.py
import httpx

BASE = "http://127.0.0.1:8000"

def main():
    with httpx.Client(base_url=BASE, follow_redirects=True) as client:
        # 1. 登入
        r = client.post("/login", data={"username": "admin", "password": "secret"})
        print(r.json())

        # 2. 取得個人資料(會自動帶上 cookie)
        r = client.get("/profile")
        print(r.json())

        # 3. 加入購物車
        r = client.post("/cart/add", json={"item_id": 42})
        print(r.json())

        # 4. 登出
        r = client.post("/logout")
        print(r.json())

if __name__ == "__main__":
    main()

執行 uvicorn main:app --reload 後,跑 python test_client.py 即可看到完整的 Session 流程。


常見陷阱與最佳實踐

陷阱 說明 解決方式
Session ID 生成不夠隨機 使用時間戳或簡易字串容易被猜測。 使用 UUID4secrets.token_urlsafe(),確保熵值足夠。
Cookie 未設定 HttpOnly / Secure 前端 JavaScript 可讀取,增加 XSS 風險。 set_cookie 時一定加上 httponly=Truesecure=True(HTTPS)以及適當的 SameSite
Session 永不過期 造成資源泄漏,且被盜用後無自動失效機制。 為每筆 Session 設定 TTL(Redis EXPIRE),並在每次存取時刷新過期時間。
在多個實例間 Session 不同步 只使用本機記憶體會導致使用者在不同節點間失去 Session。 使用 共享儲存(Redis、Memcached)或 分散式 Session 方案(如 DynamoDB、MongoDB)。
忘記標記 session_modified 改變 Session 內容後未寫回,導致資料遺失。 把「修改 Session」的程式碼封裝成 helper function,自動設定 session_modified=True
Session 內存儲過多資料 把大型檔案或大量商品資訊放入 Session,會導致 Redis 爆炸。 只儲存必要的鍵值(例如 user_id、權限、簡短的購物車 ID 列表),其餘資料透過資料庫查詢。

最佳實踐清單

  1. 使用 secrets 套件產生 Session IDsecrets.token_urlsafe(32)
  2. 設定 Cookie 為 SameSite=Lax(或 Strict),防止 CSRF。
  3. 在每次請求結束時更新 TTL,讓活躍使用者的 Session 不會因為短暫的閒置而過期。
  4. 將 Session 相關程式碼抽離為獨立模組,方便在不同專案中重複使用。
  5. 監控 Redis 記憶體與過期鍵:使用 INFOKEYSMONITOR 觀察 Session 成長趨勢,適時調整 maxmemory-policy

實際應用場景

場景 為什麼需要 Server‑Side Session
使用者登入 + JWT 佈署 即使使用 JWT 作為 API 授權,仍可透過 Session 存放 一次性驗證碼(如 2FA)或 短期授權,避免把所有資訊寫入 Token。
購物車 把商品 ID 暫存於 Session,使用者未結帳前不必寫入資料庫,減少寫入壓力;結帳時再一次性寫入持久化資料。
多步驟表單(Wizard) 每一步的暫存資料放在 Session,使用者可以在不同頁面間切換,最後一次提交完成。
權限切換 管理員在切換成普通使用者模式時,僅在 Session 中暫存 original_role,不必重新登入。
防止重複提交 在 Session 中記錄最近一次的請求指紋(如 request_id),若相同指紋再次到來則直接回傳成功或錯誤,避免重複寫入資料庫。

總結

  • Session ID + Cookie 為前端與伺服器之間的橋樑,所有敏感或大量資料皆應放在 server‑side(如 Redis)。
  • 透過 Middleware 把 Session 的讀寫抽象化,結合 依賴注入,可以在 FastAPI 中以極低的耦合度使用 Session。
  • 安全性HttpOnlySecureSameSite)與 資源管理(TTL、共享儲存)是實務上不可忽視的要點。
  • 只要把關鍵概念與實作範例內化,無論是登入驗證、購物車、或是多步驟流程,都能在 FastAPI 中以 簡潔、可擴充 的方式完成。

祝你在 FastAPI 專案中玩得開心,寫出安全、可靠且易於維護的 Session 管理程式!