FastAPI – Session‑based 認證
簡介
在 Web 應用程式中,認證是確保使用者身分合法的第一道防線。除了廣受歡迎的 JWT、OAuth2 等 token‑based 機制外,Session‑based 認證 仍是許多傳統網站與內部系統的首選。它的核心概念是:伺服器在記憶體或資料庫中保存一段「會話」資料,並透過瀏覽器的 Cookie 把會話 ID 回傳給客戶端,後續每一次請求都會帶上這個 ID,讓伺服器得以辨識使用者。
FastAPI 以 Starlette 為底層框架,天然支援 SessionMiddleware,只要稍加設定,就能在 FastAPI 中實作安全、可維護的 Session 認證。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步完成 Session‑based 認證,讓你的 API 能同時支援前端 SPA、傳統表單或行動端。
核心概念
1. Session 與 Cookie 的關係
| 項目 | Session | Cookie |
|---|---|---|
| 存放位置 | 伺服器端(記憶體、Redis、資料庫) | 客戶端(瀏覽器) |
| 內容 | 使用者身分、權限、臨時資料等 | 只存放會話 ID(如 session_id) |
| 安全性 | 只要伺服器端保護好,資料不會被竊取 | 必須使用 HttpOnly、Secure、SameSite 等屬性防止 XSS/CSRF |
重點:在 Session‑based 認證中,永遠不要把敏感資訊直接寫入 Cookie,只存會話 ID,所有實際資料都放在伺服器端。
2. FastAPI 中的 SessionMiddleware
FastAPI 直接使用 starlette.middleware.sessions.SessionMiddleware,只需要提供一個 加密金鑰(secret_key)即可讓 Cookie 中的 session ID 加密、簽名,防止被竊改。
# app/main.py
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
# 只要設定一次即可,建議使用 32 位元以上隨機字串
app.add_middleware(SessionMiddleware, secret_key="YOUR_SUPER_SECRET_KEY")
提示:
secret_key應該放在環境變數或密鑰管理系統中,絕不可硬寫在程式碼。
3. 依賴注入取得 Session
FastAPI 的依賴注入(Dependency Injection)讓我們可以在路由函式中直接取得 request.session,如同取得其他依賴一樣。
# app/dependencies.py
from fastapi import Request
def get_session(request: Request):
"""
取得當前請求的 Session dict。
若 Session 尚未建立,會自動產生空的 dict。
"""
return request.session
4. 登入、登出與 Session 的生命週期
4.1 登入流程
- 客戶端送出帳號密碼(POST
/login)。 - 後端驗證成功後,將使用者 ID(或其他必要資訊)寫入
session。 SessionMiddleware會自動在回應的 Set-Cookie 標頭中加入加密過的 session ID。
# app/routes/auth.py
from fastapi import APIRouter, Depends, HTTPException, status, Request
from passlib.context import CryptContext
router = APIRouter()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 假資料庫
FAKE_USERS_DB = {
"alice": {"username": "alice", "hashed_password": pwd_context.hash("secret123")},
}
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
@router.post("/login")
async def login(request: Request, username: str, password: str):
"""
登入 API,成功後會在 Session 中寫入 `user_id`。
"""
user = FAKE_USERS_DB.get(username)
if not user or not verify_password(password, user["hashed_password"]):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail="帳號或密碼錯誤")
# 登入成功 → 寫入 Session
request.session["user_id"] = user["username"]
# 可自行設定過期時間(秒),例如 30 分鐘
request.session["expiry"] = 30 * 60
return {"msg": "登入成功"}
4.2 登出流程
只要刪除 Session 中的資料或直接清空整個 Session 即可。
@router.post("/logout")
async def logout(request: Request):
"""
登出 API,清除 Session。
"""
request.session.clear() # 移除所有鍵值
return {"msg": "已登出"}
4.3 Session 過期與續期
SessionMiddleware 本身不會自動過期,我們需要自行在每次請求時檢查 expiry,若超過則清除 Session。
# app/dependencies.py
import time
from fastapi import Request, HTTPException, status
def get_current_user(request: Request):
"""
取得目前已登入的使用者,若未登入或 Session 過期則拋出 401。
"""
session = request.session
user_id = session.get("user_id")
expiry = session.get("expiry")
if not user_id or not expiry:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail="未登入或 Session 已失效")
# 檢查過期時間
if time.time() > session.get("created_at", time.time()) + expiry:
session.clear()
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail="Session 已過期")
# 更新最後存取時間(可選)
session["last_access"] = time.time()
return user_id
5. CSRF 防護
Session‑based 認證最常見的攻擊是 Cross‑Site Request Forgery (CSRF)。最簡單的防護方式是:
- 為所有 非 GET 請求加上自訂 Header(如
X-CSRF-Token)。 - 在登入成功時,把隨機產生的 CSRF Token 放入 Session,回傳給前端;前端在每次 POST/PUT/DELETE 時帶上此 Header。
- 後端驗證 Header 與 Session 中的 Token 是否相符。
import secrets
from fastapi import Header
# 登入成功時產生 CSRF token
@router.post("/login")
async def login(request: Request, username: str, password: str):
# ... (驗證略)
request.session["user_id"] = user["username"]
csrf_token = secrets.token_urlsafe(32)
request.session["csrf_token"] = csrf_token
return {"msg": "登入成功", "csrf_token": csrf_token}
# 依賴檢查 CSRF
def verify_csrf(request: Request, x_csrf_token: str = Header(None)):
session_token = request.session.get("csrf_token")
if not session_token or session_token != x_csrf_token:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="CSRF 驗證失敗")
# 使用範例
@router.post("/profile")
async def update_profile(
request: Request,
data: dict,
csrf: None = Depends(verify_csrf),
user_id: str = Depends(get_current_user),
):
# 實作更新使用者資料
return {"msg": f"{user_id} 的資料已更新"}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| Session 資料過大 | 將大量資料放入 request.session 會導致 Cookie 加密後變長,影響效能。 |
只存 user_id、expiry、csrf_token 等少量關鍵資訊,其他資料放在資料庫或快取(Redis)。 |
未設定 HttpOnly / Secure |
攻擊者可藉由 XSS 竊取 Cookie。 | SessionMiddleware 預設會加上 HttpOnly,若在 HTTPS 環境一定設定 Secure=True。 |
| CSRF 防護不足 | 只靠 Cookie 會遭受跨站請求。 | 如上所示,實作 雙重提交 Cookie 或 SameSite=Strict。 |
| Session 失效後仍可使用 | 未在每次請求檢查過期時間。 | 在依賴 get_current_user 中加入過期檢查,或使用 starlette 的 BackgroundTask 清除過期 Session。 |
| 密鑰外泄 | secret_key 若寫在程式碼,會被意外洩漏。 |
使用環境變數或密鑰管理服務(Vault、AWS KMS)。 |
最佳實踐清單
- 使用 HTTPS:所有 Cookie 必須設
Secure,避免明文傳輸。 - 設定
SameSite:SameSite=Lax或Strict可減少 CSRF 風險。 - 定期輪換
secret_key:配合版本升級或密鑰管理機制。 - 將 Session 存在 Redis:若應用需要水平擴展,使用集中式快取避免「sticky session」問題。
- 限制 Session 生命週期:短時間(如 15–30 分鐘)且支援「延長」機制,提升安全性。
實際應用場景
| 場景 | 為何選擇 Session‑based | 實作要點 |
|---|---|---|
| 內部管理系統(ERP、CRM) | 使用者多為公司員工,需求快速登入、角色權限切換,且不需要跨域的 token。 | 使用 SessionMiddleware + Redis,搭配角色 ID 存於 Session。 |
| 傳統表單網站(會員中心、部落格) | 前端以 HTML 表單為主,瀏覽器自動攜帶 Cookie,開發成本低。 | 設定 SameSite=Strict,並在每個 POST/PUT 加入 CSRF Token。 |
| 混合式 SPA + SSR | 部分頁面使用 Server‑Side Rendering,部分使用 Vue/React SPA,兩者共享同一 Session。 | 在 SPA 初始化時從 /login 取得 CSRF token,之後所有 API 請求都帶上 Header。 |
| 多服務微服務 | 需要在多個服務間共享使用者會話資訊。 | 將 Session 存於 Redis,所有服務共用相同 secret_key 與 Redis 連線。 |
總結
- Session‑based 認證 仍是許多企業內部與傳統網站的首選方案,核心在於伺服器端保存會話資訊、客戶端只攜帶加密過的 session ID。
- FastAPI 透過
SessionMiddleware只需簡單設定即可啟用,配合依賴注入、CSRF 防護與過期檢查,便能建立安全、可擴展的認證系統。 - 實作時要特別注意 Cookie 安全屬性、密鑰管理、Session 大小 與 CSRF 防護,並建議使用 Redis 等集中式快取以支援水平擴充。
掌握上述概念與最佳實踐,你就能在 FastAPI 中快速構建可靠的 Session‑based 認證,讓使用者體驗與系統安全同時提升。祝開發順利!