本文 AI 產出,尚未審核

FastAPI — 安全性(Security)

主題:Cookie‑based 認證


簡介

在 Web 應用程式中,認證是保護資源、辨識使用者身分的第一道防線。
雖然 JWT(JSON Web Token)在 API‑first 的設計中相當流行,但許多傳統網站仍然依賴 Cookie 來維持使用者的登入狀態。

FastAPI 本身支援多種認證方式,透過 fastapi.securitystarlette 的中介層(middleware)即可輕鬆實作 Cookie‑based 認證
本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步完成安全、可維護的 Cookie 認證流程,適合 初學者到中階開發者 參考。


核心概念

1. 為什麼選擇 Cookie?

優點 說明
自動隨請求送出 瀏覽器會自動將符合 domain、path、secure、httpOnly 條件的 Cookie 附在每一次 HTTP 請求中,前端不必額外處理 token。
支援瀏覽器原生防護 SameSiteSecureHttpOnly 等屬性讓 Cookie 在 CSRF、XSS 攻擊面上更具防禦力。
與傳統表單登入相容 多數現有的前端框架(如 Django、Flask、Rails)皆以 Cookie 為主要認證手段,易於整合。

注意:Cookie 不是萬能的,若服務是純 API(如行動端),仍建議使用 Bearer Token。

2. Cookie 的屬性與安全設定

屬性 功能 建議設定
HttpOnly 前端 JavaScript 無法讀取,防止 XSS 窃取 必設
Secure 只在 HTTPS 連線傳送,防止明文截取 必設(除非開發環境)
SameSite 限制跨站請求送出 Cookie,防止 CSRF Lax(大多數情況)或 Strict(高度安全)
Path / Domain 控制 Cookie 可被哪些路徑或子域名使用 依需求設定,預設即可

3. 認證流程概覽

  1. 使用者登入 → 後端驗證帳號密碼,若成功產生 session id(或簽名的 JWT)
  2. 設定 Cookie:在回應中使用 Set-Cookie 標頭將 token 放入瀏覽器
  3. 每次請求:瀏覽器自動帶上 Cookie,FastAPI 透過依賴(dependency)解析 token
  4. 授權驗證:根據 token 取得使用者資訊,決定是否允許存取受保護的路由

程式碼範例

以下範例使用 FastAPI 0.111Python 3.11,示範完整的 Cookie 認證流程。
所有程式碼均以 python 標記,並在關鍵行加入註解說明。

1. 建立基礎 FastAPI 專案

from fastapi import FastAPI, Request, Response, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.responses import JSONResponse
from starlette.middleware.sessions import SessionMiddleware
import secrets
import hashlib
import time

app = FastAPI()

# 加入 SessionMiddleware 讓 Starlette 能讀寫 signed cookies
app.add_middleware(
    SessionMiddleware,
    secret_key="YOUR_SUPER_SECRET_KEY",   # <-- 請換成環境變數或 vault
    session_cookie="session_id",          # Cookie 名稱
    max_age=60 * 60 * 24 * 7,             # 7 天過期
    same_site="lax",                      # SameSite 設定
    https_only=True,                      # 只在 HTTPS 傳送
)

說明
SessionMiddleware 會自動在 request.session 中提供一個 dict,背後是以簽名的 cookie 保存。若想自行管理 token,可直接使用 Response.set_cookie

2. 使用者模型與密碼雜湊(簡易示例)

# 假資料庫(實務上請使用 ORM + 加鹽雜湊)
FAKE_DB = {
    "alice": hashlib.sha256(b"alice_password").hexdigest(),
    "bob":   hashlib.sha256(b"bob_secret").hexdigest(),
}

def verify_password(username: str, password: str) -> bool:
    """比對使用者輸入的密碼是否正確"""
    stored_hash = FAKE_DB.get(username)
    if not stored_hash:
        return False
    return stored_hash == hashlib.sha256(password.encode()).hexdigest()

3. 登入端點 – 設定安全 Cookie

@app.post("/login")
async def login(credentials: HTTPBasicCredentials, response: Response):
    """
    使用 HTTP Basic 方式取得使用者帳密,成功後在 cookie 中寫入 session_id
    """
    username = credentials.username
    password = credentials.password

    if not verify_password(username, password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="帳號或密碼錯誤",
            headers={"WWW-Authenticate": "Basic"},
        )

    # 產生一個隨機且唯一的 session id(可改為 JWT)
    session_id = secrets.token_urlsafe(32)

    # 把 session_id 存到 server-side 記憶體或 Redis(此例存到 request.session)
    response.set_cookie(
        key="session_id",
        value=session_id,
        httponly=True,      # 防止 XSS
        secure=True,        # 只在 HTTPS
        samesite="lax",
        max_age=60 * 60 * 24 * 7,  # 7 天
        path="/",
    )

    # 這裡示範把 session_id 暫存到全局 dict,正式環境請改為資料庫或快取
    request.session["user"] = {"username": username, "session_id": session_id}
    return JSONResponse(content={"msg": f"歡迎 {username}!已登入"} )

重點

  • httponly=Truesecure=Truesamesite="lax"最佳實踐
  • response.set_cookie 直接寫入 Cookie,若使用 SessionMiddleware,只要把資料寫入 request.session,框架會自動處理簽名與回傳。

4. 依賴(Dependency)取得當前使用者

def get_current_user(request: Request):
    """
    從 cookie 中取得 session_id,並驗證其有效性。
    若無效則拋出 401。
    """
    session_id = request.cookies.get("session_id")
    if not session_id:
        raise HTTPException(status_code=401, detail="未提供認證資訊")

    # 這裡簡化:直接從 request.session 取出 user(實務上要比對 session_id)
    user = request.session.get("user")
    if not user or user.get("session_id") != session_id:
        raise HTTPException(status_code=401, detail="認證失敗或已過期")
    return user

5. 受保護的 API

@app.get("/profile")
async def read_profile(user: dict = Depends(get_current_user)):
    """
    只有已登入且 session 有效的使用者才能存取此路由。
    """
    return {"username": user["username"], "message": "這是你的個人資料"}

6. 登出端點 – 清除 Cookie

@app.post("/logout")
async def logout(response: Response, request: Request):
    """
    移除 server-side session 並刪除 client-side cookie。
    """
    # 清除 server 端的 session(若使用 Redis、DB 等,請同步刪除)
    request.session.clear()

    # 設定過期時間為 0,讓瀏覽器刪除 cookie
    response.delete_cookie(
        key="session_id",
        path="/",
        domain=None,   # 若有自訂 domain,請一併填寫
    )
    return {"msg": "已成功登出"}

7. CSRF 防護(可選)

若前端使用 POST、PUT、DELETE 等非安全方法,建議再加上 CSRF token

import uuid

@app.get("/csrf-token")
async def get_csrf_token(response: Response):
    token = str(uuid.uuid4())
    response.set_cookie(
        key="csrf_token",
        value=token,
        httponly=False,   # 必須讓前端 JS 讀取
        secure=True,
        samesite="lax",
        max_age=60 * 30,  # 30 分鐘有效
    )
    return {"csrf_token": token}

在受保護的 POST API 中驗證:

from fastapi import Header

@app.post("/update-profile")
async def update_profile(
    data: dict,
    csrf_token: str = Header(..., alias="X-CSRF-Token"),
    request: Request = Depends(),
):
    # 取出 cookie 中的 token
    cookie_token = request.cookies.get("csrf_token")
    if not cookie_token or csrf_token != cookie_token:
        raise HTTPException(status_code=403, detail="CSRF 驗證失敗")
    # ... 進行更新
    return {"msg": "資料已更新"}

常見陷阱與最佳實踐

陷阱 說明 最佳做法
忘記設定 HttpOnly 前端腳本可讀取 Cookie,易被 XSS 攻擊竊取。 必設 httponly=True
在測試環境未關閉 Secure 本機 HTTP 無法傳送 Secure Cookie,導致登入失敗。 開發時可暫時 secure=False,或使用 HTTPS 的本機測試環境(如 mkcert)。
使用過長的 Session ID 會增加 Cookie 大小,超過瀏覽器限制(約 4KB)。 產生 32~64 位元 的隨機字串即可。
未限制 SameSite 跨站請求會自動攜帶 Cookie,增加 CSRF 風險。 設為 lax(大多數情況)或 strict(高度安全)。
把敏感資訊直接寫入 Cookie Cookie 會被瀏覽器儲存,可能被竊取或被瀏覽器快取。 只存 Session ID,其餘資訊放在伺服器端(DB、Redis)。
忘記在登出時清除 Session 仍可使用舊的 Cookie 重新登入,形成會話固定(session fixation)攻擊。 response.delete_cookie 並同步刪除伺服器端資料。
未對 Session ID 做簽名或加密 攻擊者可自行偽造合法的 Session ID。 使用 itsdangerousJWTSessionMiddleware 內建簽名。

其他安全建議

  1. 使用 HTTPS:所有 Cookie(尤其是 Secure)必須在 TLS 加密下傳輸。
  2. 設定適當的過期時間:根據業務需求決定 max_age,過長會增加被盜用的風險。
  3. 監控異常登入:如同一 IP 短時間內多次失敗,應暫時封鎖或要求 CAPTCHA。
  4. 分層授權:即使已驗證,仍需在路由層級檢查使用者角色或權限。

實際應用場景

場景 為何適合 Cookie 認證 實作要點
企業內部管理系統 使用者大多透過瀏覽器操作,且需要 SSO 整合。 結合 OAuth2AuthorizationCodeBearerSessionMiddleware,在登入成功後寫入 session_id
電商平台的購物車 購物車資訊需要在多個子域名間共享。 設定 Domain=.example.comSameSite=None(配合 Secure)讓跨子域名的 Cookie 可用。
教育平台的課程觀看 需要限制已登入的學生才能觀看影片。 在影片 API 加入 Depends(get_current_user),並在前端使用 fetch 自動帶上 Cookie。
混合型前端(SSR + SPA) 首頁用 SSR 渲染,後續切換為 SPA,仍需保持認證。 在 SSR 階段使用 request.session 注入使用者資訊,SPA 階段則透過 get_current_user 取得。
需要防止 CSRF 的表單提交 多數表單使用 POST,若僅靠 Cookie 容易被偽造。 加入 CSRF token 機制(如上例),或改用 SameSite=Strict

總結

  • Cookie‑based 認證 在傳統 Web 應用中仍是最常見且最便利的方式,只要正確設定 HttpOnlySecureSameSite 等屬性,就能抵禦大部分 XSS、CSRF 攻擊。
  • FastAPI 與 Starlette 提供了 SessionMiddlewareResponse.set_cookie、依賴注入等工具,讓開發者可以在幾行程式碼內完成安全的登入、驗證與登出流程。
  • 實務上,別把敏感資料直接寫入 Cookie,只存 session_id,其餘資訊保留在伺服器端(DB、Redis、Cache)。
  • 針對 跨站請求,建議同時使用 CSRF tokenSameSite=Strict,並確保所有通訊皆在 HTTPS 下。

透過本文的概念說明與完整範例,你現在應該能在 FastAPI 中快速實作安全、可擴充的 Cookie 認證機制,並根據不同的業務需求調整設定與最佳實踐。祝開發順利,打造更安全的 Web 應用!