FastAPI – Session 與 Cookie 管理
主題:簽章 Cookie(Secure Cookie)
簡介
在 Web 應用程式中,Cookie 是最常見的用戶端儲存機制之一。它可以用來保存使用者的登入資訊、偏好設定,甚至是跨請求的狀態(session)。然而,普通的 Cookie 僅是明文儲存,若被竊取或竄改,將直接危及應用程式的安全性。
簽章 Cookie(Signed Cookie) 透過在 Cookie 值上加入 HMAC 簽名,使得伺服器在每次收到 Cookie 時都能驗證其完整性與來源。結合 Secure、HttpOnly、SameSite 等屬性,便能打造既 安全 又 易於維護 的認證機制。
本篇文章將以 FastAPI 為例,說明如何在 Python 生態系統中正確產生、驗證與管理簽章 Cookie,並提供實作範例、常見陷阱與最佳實踐,幫助你在真實專案中即時上手。
核心概念
1. 為什麼需要簽章 Cookie?
- 防止竄改:未簽章的 Cookie 可以被瀏覽器外的工具(如開發者工具)直接編輯。加入簽章後,若內容被改動,簽名驗證會失敗,伺服器即可拒絕請求。
- 減少伺服器端儲存:傳統的 session 需要在伺服器端保留資料表或快取;簽章 Cookie 把資料直接放在客戶端,伺服器只需要保存簽名金鑰即可。
- 跨服務一致性:在微服務或多個子域名的環境中,只要金鑰一致,各服務都能驗證同一個 Cookie,降低同步成本。
2. 簽章的基本原理
- 產生資料(如
user_id、exp等) - 將資料序列化(JSON → Base64)
- 使用 HMAC‑SHA256 與密鑰產生簽名
- 把
payload.signature以.為分隔組合成最終 Cookie 值
收到請求時,伺服器會:
- 把 Cookie 拆成
payload與signature - 用相同金鑰重新計算簽名
- 比對兩個簽名是否相等,若相等則視為有效。
3. FastAPI 中的實作方式
FastAPI 本身不提供簽章 Cookie 的工具,但可以結合 itsdangerous(Flask 的簽章套件)或 python‑jose、cryptography 來完成。以下示範使用 itsdangerous.URLSafeTimedSerializer,因為它同時支援 時效(Timed)與 URL 安全(Base64 URL safe)編碼。
程式碼範例
以下範例均基於 FastAPI 0.110+ 與 Python 3.11+。
為了方便說明,所有範例放在同一個main.py,實際專案建議依功能拆分模組。
1️⃣ 基本設定與工具函式
# main.py
from fastapi import FastAPI, Request, Response, HTTPException, Depends
from fastapi.responses import JSONResponse
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
from datetime import timedelta
app = FastAPI()
# 產生簽章用的密鑰,請務必從環境變數或安全管理系統讀取
SECRET_KEY = "YOUR_SUPER_SECRET_KEY_CHANGE_ME"
COOKIE_NAME = "session_token"
# 1 天過期
COOKIE_MAX_AGE = 60 * 60 * 24
serializer = URLSafeTimedSerializer(SECRET_KEY)
2️⃣ 產生簽章 Cookie
def create_signed_cookie(data: dict, max_age: int = COOKIE_MAX_AGE) -> str:
"""
把任意字典資料簽章後回傳字串,適合作為 Cookie value。
"""
# data 會自動序列化為 JSON → Base64
token = serializer.dumps(data)
return token
3️⃣ 驗證簽章 Cookie
def verify_signed_cookie(token: str, max_age: int = COOKIE_MAX_AGE) -> dict:
"""
驗證 Cookie 是否未被竄改且未過期,成功回傳原始資料。
若驗證失敗會拋出例外。
"""
try:
data = serializer.loads(token, max_age=max_age)
return data
except SignatureExpired:
raise HTTPException(status_code=401, detail="Cookie 已過期")
except BadSignature:
raise HTTPException(status_code=401, detail="Cookie 簽章無效")
4️⃣ 登入端點 – 發送簽章 Cookie
@app.post("/login")
async def login(response: Response, username: str, password: str):
"""
假設有一個簡易的驗證機制,成功後回傳簽章 Cookie。
"""
# 這裡僅示範,實務請使用資料庫與安全的密碼雜湊驗證
if username != "admin" or password != "secret":
raise HTTPException(status_code=401, detail="帳號或密碼錯誤")
# 建立要放入 Cookie 的資料,通常只放最小必要資訊
payload = {
"sub": username, # 使用者代號
"role": "admin", # 角色資訊(視需求自行決定)
"exp": (datetime.utcnow() + timedelta(seconds=COOKIE_MAX_AGE)).timestamp(),
}
token = create_signed_cookie(payload)
# 設定 Secure、HttpOnly、SameSite 等屬性
response.set_cookie(
key=COOKIE_NAME,
value=token,
max_age=COOKIE_MAX_AGE,
httponly=True, # 防止 JavaScript 讀取
secure=True, # 只在 HTTPS 下傳送
samesite="lax", # 防止 CSRF(根據需求可改為 strict)
)
return {"msg": "登入成功"}
5️⃣ 受保護的 API – 讀取並驗證 Cookie
def get_current_user(request: Request) -> dict:
"""
依賴注入函式,從 Cookie 讀取使用者資訊。
"""
token = request.cookies.get(COOKIE_NAME)
if not token:
raise HTTPException(status_code=401, detail="未提供認證 Cookie")
return verify_signed_cookie(token)
@app.get("/profile")
async def read_profile(user: dict = Depends(get_current_user)):
"""
只有在簽章驗證通過後才會執行,回傳使用者資訊。
"""
return JSONResponse(content={"username": user["sub"], "role": user["role"]})
6️⃣ 登出端點 – 清除 Cookie
@app.post("/logout")
async def logout(response: Response):
"""
透過設定過期時間為 0,讓瀏覽器刪除 Cookie。
"""
response.delete_cookie(key=COOKIE_NAME, httponly=True, secure=True, samesite="lax")
return {"msg": "已登出"}
7️⃣ 使用 SameSite=None 時的額外設定(跨子域名)
# 若前端與 API 分別部署在不同子域名,且需要跨站傳送 Cookie,使用 SameSite=None
response.set_cookie(
key=COOKIE_NAME,
value=token,
max_age=COOKIE_MAX_AGE,
httponly=True,
secure=True, # SameSite=None 必須同時設定 Secure
samesite="none",
)
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 金鑰外洩 | 若 SECRET_KEY 被泄漏,攻擊者可自行簽發合法 Cookie。 |
使用環境變數、Vault 或 KMS 儲存金鑰;定期輪換金鑰並提供舊金鑰驗證過渡期。 |
| 過長的 Cookie | 把過多資料放入 Cookie 會導致請求頭過大,瀏覽器甚至拒絕發送。 | 只放最小必要資訊(如使用者 ID、過期時間、角色代號),其餘資訊可放在資料庫或快取。 |
| 未設定 Secure | 在 HTTP 下傳送 Cookie 會被明文截取。 | 永遠設定 secure=True,且在測試環境使用自行簽章的 HTTPS。 |
| 忘記 HttpOnly | JavaScript 可讀取 Cookie,容易被 XSS 攻擊盜走。 | 設定 httponly=True,除非真的需要前端讀取(如前端框架的 CSRF token)。 |
| SameSite 設定不當 | SameSite=Lax 在某些跨站 POST 情境下會被瀏覽器阻擋。 |
根據業務需求選擇 Lax、Strict 或 None,但 None 必須同時啟用 Secure。 |
| 時效驗證錯誤 | 使用 itsdangerous 時忘記傳入 max_age,導致永遠不會過期。 |
明確傳入 max_age,或自行在 payload 中加入 exp 並在驗證時檢查。 |
| 多服務金鑰不一致 | 微服務間金鑰不同,導致無法互相驗證。 | 使用集中式金鑰管理,或在部署腳本中同步金鑰。 |
最佳實踐小結
- 金鑰管理:使用安全的儲存方式,並支援金鑰輪換。
- 最小化資料:只把「身份識別」與「過期時間」放入 Cookie。
- 屬性設定:
Secure + HttpOnly + SameSite為基本安全三劍客。 - 時效機制:結合
itsdangerous的 timed serializer 或自行檢查exp。 - 測試:在本機使用自簽 HTTPS 測試
Secure與SameSite=None行為。
實際應用場景
| 場景 | 為什麼適合使用簽章 Cookie |
|---|---|
| 單頁應用 (SPA) + API | 前端不需要自行管理 token,只要瀏覽器自動帶上 Cookie,後端即可驗證使用者身份。 |
| 微服務架構 | 多個子服務共享同一個簽章金鑰,使用者只登入一次即可在不同服務間通行。 |
| 無狀態 API | 免除伺服器端 Session 資料庫,減少記憶體與 I/O 負載。 |
| 第三方 SSO 整合 | 在 SSO 成功後產生簽章 Cookie,讓內部服務能快速驗證使用者。 |
| 行動端 WebView | 行動應用內嵌的 WebView 只要支援 Cookie,便能使用相同的認證機制。 |
範例:在一個電商平台中,使用者登入後產生
session_token,前端透過axios自動攜帶 Cookie 呼叫/order,/profile等保護路由;若使用者在另一個子域名(如admin.example.com)登入管理介面,仍能使用同一個 Cookie 進行驗證,因為所有服務共用相同的SECRET_KEY。
總結
簽章 Cookie 為 FastAPI 應用提供了一條 輕量且安全 的認證道路。透過 itsdangerous(或類似的 HMAC 套件)可以快速完成 資料簽名、時效驗證與完整性檢查;再加上 Secure、HttpOnly、SameSite 等屬性的正確設定,便能大幅降低 XSS、CSRF 與 session 劫持的風險。
本文從 概念說明、完整程式範例、常見陷阱、最佳實踐 到 實務應用場景,提供了一套可直接套用於專案的解決方案。只要遵守以下幾點:
- 保管好金鑰,並支援金鑰輪換。
- 只放最小必要資訊 在 Cookie 中。
- 設定
Secure + HttpOnly + SameSite。 - 使用時效驗證 防止永久有效的 Cookie。
就能在 FastAPI 中構建一個既 安全 又 高效 的 Session 管理機制,讓開發者把更多精力放在業務功能上,而非認證細節。
祝你在 FastAPI 專案中玩得開心,寫出更安全、更可靠的 Web 服務! 🚀