FastAPI ── Session 與 Cookie 管理
主題:設定與讀取 Cookie
簡介
在 Web 開發中,Cookie 是前端與後端之間最常見的狀態保存機制之一。它可以用來記錄使用者的登入資訊、偏好設定、或是臨時的會話資料。對於使用 FastAPI 建置 API 服務時,正確地 設定、讀取 以及 保護 Cookie,是提升使用者體驗與安全性的關鍵。
本篇文章將從 概念、實作範例、常見陷阱 以及 最佳實踐 四個面向,深入說明如何在 FastAPI 中操作 Cookie,讓你能在實務專案中快速上手,從初學者一路晉升為中級開發者。
核心概念
1. Cookie 的基本結構
Cookie 本質上是一個鍵值對,由伺服器在回應 (Response) 中透過 Set-Cookie 標頭送出,瀏覽器隨後在每次請求 (Request) 時自動帶回。常見屬性包括:
| 屬性 | 說明 |
|---|---|
| Expires / Max-Age | 設定 Cookie 的有效期限 |
| Domain | 限定 Cookie 可被哪些子網域使用 |
| Path | 限定 Cookie 在哪個路徑下會被送回 |
| Secure | 只允許在 HTTPS 連線中傳送 |
| HttpOnly | 前端 JavaScript 無法存取,防止 XSS 攻擊 |
| SameSite | 防止 CSRF,值可為 Lax、Strict、None |
2. FastAPI 中的 Cookie 操作介面
FastAPI 內建兩個與 Cookie 互動的物件:
Response(或其子類別JSONResponse、HTMLResponse)提供set_cookie()方法。Request提供cookies屬性,可直接以字典方式取值。
小技巧:若想在路由函式內同時使用
Request、Response,只需要在參數中同時聲明兩者,FastAPI 會自動注入實例。
3. 為什麼要簽名 (signed) Cookie?
直接把敏感資訊(如使用者 ID、權杖)寫入 Cookie 會有 資訊被竄改 的風險。使用 簽名(或加密)後,即使使用者可以看到 Cookie 的內容,也無法自行修改而不被伺服器偵測。FastAPI 常搭配 itsdangerous 套件完成簽名與驗證。
程式碼範例
以下示範 4 個實用範例,涵蓋 設定、讀取、簽名、以及 依賴注入 的寫法。每段程式碼都加上說明註解,方便直接複製到專案中使用。
範例 1:最簡單的 Set‑Cookie
# main.py
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/set-cookie")
def set_cookie(response: Response):
"""
直接在回應中設定一個名為 `visited` 的 Cookie,值為 `yes`。
預設為 Session Cookie(瀏覽器關閉即失效)。
"""
response.set_cookie(key="visited", value="yes")
return {"message": "Cookie 已設定"}
重點:
set_cookie的key為 Cookie 名稱,value為字串值。若未指定max_age或expires,則為 Session Cookie。
範例 2:讀取 Cookie
# main.py
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.get("/read-cookie")
def read_cookie(request: Request):
"""
從 Request 物件的 `cookies` 取出名為 `visited` 的 Cookie。
若不存在則回傳 400 錯誤。
"""
visited = request.cookies.get("visited")
if not visited:
raise HTTPException(status_code=400, detail="Cookie not found")
return {"visited": visited}
提示:
request.cookies回傳的是 MutableMapping,可直接使用字典操作。
範例 3:設定帶有安全屬性的 Cookie
# main.py
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/set-secure-cookie")
def set_secure_cookie(response: Response):
"""
設定一個只能在 HTTPS 中傳送,且無法被 JavaScript 存取的 Cookie。
同時設定 SameSite 為 Lax,降低 CSRF 風險。
"""
response.set_cookie(
key="session_id",
value="random_generated_token",
max_age=60 * 60 * 24, # 1 天
httponly=True, # 前端無法透過 document.cookie 讀取
secure=True, # 必須在 https 才會傳送
samesite="lax", # 防止跨站請求偽造
)
return {"message": "Secure cookie set"}
最佳實踐:對於 認證相關 的 Cookie,務必同時開啟
httponly、secure、samesite。
範例 4:使用 itsdangerous 簽名 Cookie(防止竄改)
# main.py
from fastapi import FastAPI, Response, Request, HTTPException
from itsdangerous import URLSafeSerializer, BadSignature
app = FastAPI()
# 建議把 secret_key 放在環境變數或設定檔中
serializer = URLSafeSerializer("超級機密的密鑰")
@app.get("/login")
def login(response: Response, user_id: int):
"""
登入成功後,將使用者 ID 以簽名方式寫入 Cookie。
前端只能拿到簽名後的字串,無法自行改動。
"""
signed_user_id = serializer.dumps(user_id)
response.set_cookie(
key="auth_token",
value=signed_user_id,
max_age=3600, # 1 小時
httponly=True,
secure=True,
samesite="strict",
)
return {"message": "Logged in"}
@app.get("/profile")
def profile(request: Request):
"""
讀取並驗證簽名的 Cookie,若驗證失敗則拋出 401。
"""
token = request.cookies.get("auth_token")
if not token:
raise HTTPException(status_code=401, detail="未提供 auth_token")
try:
user_id = serializer.loads(token)
except BadSignature:
raise HTTPException(status_code=401, detail="Cookie 簽名驗證失敗")
return {"user_id": user_id, "profile": "這裡放使用者資料"}
說明:
URLSafeSerializer會產生類似WyIxMjMiXQ==的字串,既可安全放入 Cookie,又能在伺服器端解碼驗證。
範例 5:利用 Depends 抽象出 Cookie 讀取邏輯
# main.py
from fastapi import FastAPI, Depends, Request, HTTPException
app = FastAPI()
def get_current_user(request: Request):
"""
依賴函式:統一從 Cookie 取得使用者 ID,若失敗則拋出例外。
這樣在每個需要驗證的路由上,只要加入 `current_user: int = Depends(get_current_user)`
即可。
"""
token = request.cookies.get("auth_token")
if not token:
raise HTTPException(status_code=401, detail="未登入")
# 這裡直接假設 token 為 user_id,實務上仍建議使用簽名驗證
try:
user_id = int(token)
except ValueError:
raise HTTPException(status_code=401, detail="Invalid token")
return user_id
@app.get("/dashboard")
def dashboard(current_user: int = Depends(get_current_user)):
return {"msg": f"歡迎使用者 {current_user} 進入儀表板"}
好處:把驗證邏輯抽離成依賴,可讓路由保持乾淨,且容易在未來加入簽名或 JWT 驗證。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 建議的解決方案 |
|---|---|---|
忘記設定 HttpOnly |
前端 JavaScript 可讀取 Cookie,增加 XSS 竊取風險。 | 務必 為認證或會話類 Cookie 加上 httponly=True。 |
在 HTTP 環境下使用 Secure |
Cookie 不會被瀏覽器送出,導致登入失效。 | 僅在正式部署的 HTTPS 環境開啟 secure=True,開發時可暫時關閉。 |
SameSite 設為 None 卻未加 Secure |
現代瀏覽器會直接拋棄 Cookie。 | 若需跨站請求,使用 samesite="none" 並同時設定 secure=True。 |
| 在 Cookie 中存放過大資料 | 超過 4KB 限制會導致 Cookie 被截斷或丟失。 | 只存放必要的辨識碼(如 session_id),其餘資料放在伺服器端儲存。 |
| 直接寫入明文敏感資訊 | 可能被竊取或自行篡改。 | 使用 簽名/加密(itsdangerous、JWE)或改用 JWT、Redis Session。 |
| 未設定有效期限 | Cookie 成為 Session Cookie,關閉瀏覽器即失效,使用者體驗受影響。 | 根據需求設定 max_age 或 expires,並配合 refresh 機制。 |
總結最佳實踐
- 最小化 Cookie 內容:只存放唯一識別碼(例如
session_id),所有業務資料放在資料庫或快取。 - 啟用安全屬性:
httponly=True、secure=True(HTTPS)以及合適的samesite。 - 使用簽名或加密:防止 Cookie 被惡意修改。
- 統一驗證邏輯:透過 FastAPI 的
Depends抽象出驗證流程,保持程式碼乾淨。 - 妥善設定過期:依需求設定
max_age,並在必要時提供刷新(refresh)端點。
實際應用場景
| 場景 | 為何使用 Cookie | 相關程式碼片段 |
|---|---|---|
| 使用者登入認證 | 將簽名後的 user_id 或 JWT 放入 auth_token,每次請求自動攜帶。 |
範例 4 中的 login / profile。 |
| 語系或主題偏好 | 只需保存 lang=zh-TW、theme=dark,讓前端在每次載入時直接套用。 |
response.set_cookie(key="lang", value="zh-TW", max_age=30*24*3600)。 |
| A/B 測試分組 | 伺服器在首次訪問時分配組別編號,寫入 Cookie,之後保持同一組別。 | response.set_cookie(key="ab_group", value="B", max_age=7*24*3600)。 |
| CSRF Token | 伺服器產生一次性 token,寫入 SameSite=None 且 Secure 的 Cookie,前端再從表單或 Header 中讀取驗證。 |
response.set_cookie(key="csrf_token", value=token, httponly=False, secure=True, samesite="strict")。 |
| 跨子域共享登入狀態 | 設定 Domain=.example.com,讓 app.example.com、admin.example.com 共用同一 Cookie。 |
response.set_cookie(key="session_id", value=..., domain=".example.com")。 |
總結
本文從 Cookie 的基本概念 出發,說明了在 FastAPI 中如何 設定、讀取,以及 保護 Cookie 的全套流程。透過 4~5 個實作範例,我們展示了:
- 基本的
set_cookie與request.cookies用法 - 加入 安全屬性(
HttpOnly、Secure、SameSite)的最佳做法 - 使用 itsdangerous 簽名 Cookie,防止被竄改
- 利用 Depends 抽象驗證邏輯,提升程式碼可維護性
最後,我們列舉了常見的 陷阱、最佳實踐,以及 實務場景,希望讀者在開發 API 時,能夠 安全、有效率 地管理 Session 與 Cookie,為使用者提供更好的體驗與更高的安全性。
祝開發順利,快把這些技巧寫進你的 FastAPI 專案吧! 🚀