本文 AI 產出,尚未審核

FastAPI – 安全性(Security)

token 驗證與過期機制


簡介

Web API 中,最常見的授權方式就是 JWT(JSON Web Token)。它不僅能攜帶使用者的身份資訊,還能在不需要每次都查詢資料庫的情況下完成授權驗證,極大提升效能。
然而,若沒有妥善處理 token 的驗證與過期,就會出現安全漏洞,例如被盜用的 token 永遠有效、或是過期的 token 仍被接受,進而造成資料外洩或服務被濫用。

本單元將說明在 FastAPI 中如何實作 token 的簽發、驗證、以及自動過期,並提供完整範例、常見陷阱與最佳實踐,讓你在開發 API 時能快速且安全地使用 JWT。


核心概念

1. JWT 的基本結構

JWT 由三個部份組成,使用 . 分隔:

header.payload.signature
部份 內容 說明
header 例如 {"alg":"HS256","typ":"JWT"} 指定簽名演算法
payload 例如 {"sub":"user_id","exp":1700000000} 放置使用者資訊與過期時間 (exp)
signature HMAC SHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) 防止資料被竄改

重點exp(Expiration Time)是 JWT 內建的過期欄位,FastAPI 會依此欄位自動判斷 token 是否已過期。

2. 為什麼要設定過期時間?

  • 降低被盜用的風險:即使攻擊者取得 token,也只能在有效期間內使用。
  • 強制使用者重新驗證:過期後必須重新登入或使用 refresh token 取得新 token。
  • 符合資安合規:許多法規(如 GDPR、PCI‑DSS)要求「最小權限」與「最短存活時間」的認證機制。

3. 常見的驗證流程

  1. 使用者登入 → 後端驗證帳號/密碼。
  2. 後端產生 JWT(包含 subexpiat 等欄位),回傳給前端。
  3. 前端將 token 存於 Authorization HeaderBearer <token>
  4. 每次呼叫受保護的 API,FastAPI 會解析 Header、驗證簽名與過期時間。
  5. 若驗證成功,Depends 會把使用者資訊注入路由函式;失敗則拋出 401。

程式碼範例

以下範例採用 Python 3.9+FastAPIPyJWTpython-jose)以及 Passlib 來完成完整的登入、產生 token、驗證以及過期機制。

3.1 安裝必要套件

pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt]

3.2 建立 auth.py:Token 產生與驗證工具

# auth.py
from datetime import datetime, timedelta
from typing import Optional

from jose import JWTError, jwt
from passlib.context import CryptContext

# ------------------------------------------------------------
# 設定
# ------------------------------------------------------------
SECRET_KEY = "YOUR_SUPER_SECRET_KEY_CHANGE_THIS"   # 請務必使用環境變數
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30   # Access Token 有效期 30 分鐘

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# ------------------------------------------------------------
# 密碼雜湊與驗證
# ------------------------------------------------------------
def get_password_hash(password: str) -> str:
    """將明文密碼雜湊"""
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """驗證明文密碼與雜湊是否相符"""
    return pwd_context.verify(plain_password, hashed_password)

# ------------------------------------------------------------
# JWT 產生
# ------------------------------------------------------------
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """
    建立 JWT。
    - `data` 必須包含 `sub`(使用者唯一識別碼)。
    - `expires_delta` 若未提供,預設使用 ACCESS_TOKEN_EXPIRE_MINUTES。
    """
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
    to_encode.update({"exp": expire, "iat": datetime.utcnow()})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# ------------------------------------------------------------
# JWT 驗證
# ------------------------------------------------------------
def decode_access_token(token: str) -> dict:
    """
    解析 JWT,若驗證失敗或已過期會拋出 JWTError。
    回傳 payload(已解碼的字典)。
    """
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload
    except JWTError as e:
        raise e

3.3 建立 main.py:FastAPI 路由與依賴

# main.py
from datetime import timedelta
from fastapi import FastAPI, Depends, HTTPException, status, Security
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import Optional

from auth import (
    get_password_hash,
    verify_password,
    create_access_token,
    decode_access_token,
)

app = FastAPI(title="FastAPI JWT 範例")

# ------------------------------------------------------------
# 模擬資料庫(實務上請改成真實 DB)
# ------------------------------------------------------------
fake_users_db = {
    "alice@example.com": {
        "username": "alice",
        "full_name": "Alice Wonderland",
        "hashed_password": get_password_hash("secret123"),
        "disabled": False,
    }
}

# ------------------------------------------------------------
# OAuth2 設定(使用 Bearer Token)
# ------------------------------------------------------------
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

def get_current_user(token: str = Depends(oauth2_scheme)):
    """
    依賴:從 Header 取得 token,驗證簽名與過期時間,
    若成功回傳使用者資訊,失敗則拋出 401。
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="無效的驗證資訊",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = decode_access_token(token)
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except Exception:
        raise credentials_exception

    user = fake_users_db.get(username)
    if user is None:
        raise credentials_exception
    return user

# ------------------------------------------------------------
# 登入端點:取得 Access Token
# ------------------------------------------------------------
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    使用者以 `username` + `password` 登入,
    若驗證成功回傳 JWT(access_token)。
    """
    user_dict = fake_users_db.get(form_data.username)
    if not user_dict:
        raise HTTPException(status_code=400, detail="使用者不存在")
    if not verify_password(form_data.password, user_dict["hashed_password"]):
        raise HTTPException(status_code=400, detail="密碼錯誤")

    access_token_expires = timedelta(minutes=30)
    access_token = create_access_token(
        data={"sub": user_dict["username"]},
        expires_delta=access_token_expires,
    )
    return {"access_token": access_token, "token_type": "bearer"}

# ------------------------------------------------------------
# 受保護的範例 API
# ------------------------------------------------------------
@app.get("/users/me")
async def read_current_user(current_user: dict = Depends(get_current_user)):
    """
    只有持有有效且未過期的 token 才能呼叫此端點。
    """
    return {"username": current_user["username"], "full_name": current_user["full_name"]}

# ------------------------------------------------------------
# 測試過期機制:手動產生已過期的 token
# ------------------------------------------------------------
@app.get("/test/expired")
async def test_expired_token():
    """
    產生一個已過期的 token(-5 分鐘),用於測試 401 回應。
    """
    expired_token = create_access_token(
        data={"sub": "alice"},
        expires_delta=timedelta(minutes=-5),
    )
    return {"expired_token": expired_token}

3.4 使用 uvicorn 啟動服務

uvicorn main:app --reload

測試流程

  1. 取得 token

    curl -X POST "http://127.0.0.1:8000/token" -H "Content-Type: application/x-www-form-urlencoded" -d "username=alice@example.com&password=secret123"
    

    會得到類似:

    {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","token_type":"bearer"}
    
  2. 呼叫受保護 API

    curl -H "Authorization: Bearer <access_token>" http://127.0.0.1:8000/users/me
    
  3. 驗證過期
    取得 /test/expired 回傳的 token,帶入同樣的 Header,會收到 401 Unauthorized,證明過期機制生效。


常見陷阱與最佳實踐

陷阱 可能的後果 建議的最佳實踐
使用固定的 SECRET_KEY 攻擊者若取得程式碼即能偽造所有 token。 將 secret 存於環境變數或密鑰管理服務(如 AWS Secrets Manager)。
未設定 exp 欄位 token 永遠有效,風險極高。 必須在產生 JWT 時加入 exp,且設定合理的存活時間(如 15~60 分鐘)。
過長的過期時間 使用者長時間未登出仍可使用舊 token。 根據業務需求調整過期時間,搭配 refresh token
在前端將 token 存於 localStorage 容易受到 XSS 攻擊。 使用 HttpOnly、Secure 的 Cookie,或確保前端避免 XSS。
未驗證 sub 欄位 可能會把錯誤的使用者資訊注入系統。 驗證 payload 中的 sub 是否對應真實使用者,且檢查 issaud 等欄位(若有需求)。
使用同步的密碼雜湊 大量登入請求時會阻塞事件迴圈。 使用 passlib 的 async 版本或在背景執行緒中處理

其他安全建議

  • HTTPS:所有 API 必須走 TLS,防止 token 被竊聽。
  • CORS 設定:僅允許可信任的前端來源。
  • Rate Limiting:對登入端點實施速率限制,降低暴力破解風險。
  • Refresh Token:使用短期 Access Token 搭配長期 Refresh Token,提升使用者體驗與安全性。
  • Token 黑名單:在使用者登出或密碼變更時,將舊 token 加入黑名單(如 Redis),即使未過期也會被拒絕。

實際應用場景

  1. 企業內部系統
    員工登入後取得 30 分鐘的 Access Token,前端每次呼叫 API 時自動帶入。若超過期限,前端使用 Refresh Token 取得新 Access Token,無需再次輸入密碼。

  2. 行動 App
    手機端儲存 JWT 於 Secure Enclave,並在每次 API 呼叫時加上 Bearer Header。若使用者切換設備,舊 token 立即失效(透過黑名單機制),新設備必須重新授權。

  3. 微服務間授權
    服務 A 產生 JWT(包含 roles 權限),服務 B 在收到請求時驗證 token 並根據 roles 決定是否允許存取特定資源,確保服務間的最小權限原則。


總結

  • JWT 是 FastAPI 中最常見且高效的授權方式。
  • 過期機制 (exp) 是保護 token 不被長期濫用的關鍵,務必在產生時明確設定。
  • 透過 依賴注入 (Depends) 搭配 OAuth2PasswordBearer,可以在路由層級輕鬆完成驗證。
  • 最佳實踐 包括使用環境變數管理密鑰、HTTPS、短效 Access Token + Refresh Token、以及必要的黑名單機制。

掌握以上概念與範例後,你就能在 FastAPI 專案中安全、快速地實作 token 驗證與過期機制,為 API 提供可靠的保護層。祝開發順利,系統安全無虞!