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 管理。SessionMiddleware 是 Starlette(FastAPI 的底層框架)提供的中間層,負責:
- 讀取/寫入 Cookie 中的 Session ID
- 在
request.state.session上掛載一個可變的字典,讓路由函式直接存取會話資料 - 在回應階段自動把更新過的 Session 資料寫回 Cookie(或其他儲存後端)
使用 SessionMiddleware 後,我們可以像操作普通字典一樣管理會話,免除自行編寫 Cookie 解析與加密的繁雜工作。
3. Session 的儲存方式
SessionMiddleware 預設使用 簽名的 Cookie(signed)直接在客戶端保存會話資料,適合小量、非機密的資訊。若需要更安全或更大的儲存空間,常見的做法是:
- 伺服器端儲存:將 Session ID 存於 Cookie,真正的會話資料放在 Redis、資料庫或檔案系統。
- 加密 Cookie:使用
itsdangerous或cryptography把資料加密後寫入 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": "這是使用者個人資料"}
說明
- Session ID 只是一個 UUID,存於 Cookie 中;真正的資料放在 Redis。
request.state.session為 Python dict,可在任何路由或依賴中直接讀寫。HttpOnly、Secure(若使用 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 進行原子寫入 |
其他最佳實踐
- 最小化 Session 資料:只存放必要的鍵值(例如
user_id),其他資訊可在需要時再查 DB。 - 使用類型安全的 Session 介面:可自行封裝
SessionDict,限制只能存放 JSON‑serializable 物件。 - 監控與日誌:記錄 Session 建立、失效、異常簽名等事件,方便安全審計。
- 分離測試環境:測試時使用
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 的更多可能! 🚀