FastAPI JWT Token 實作登入機制

上一章我們實作了登入流程並使用密碼雜湊來保護帳號。然而,當時回傳的 Token 只是一個簡單的字串 (username),這在實務上無法滿足安全性與效能需求。

現代 Web API 最主流的解決方案是 JWT (JSON Web Token)

什麼是 JWT?

JWT 是一個開放標準 (RFC 7519),它定義了一種緊湊且自包含的方式,用於在各方之間以 JSON 物件安全地傳輸資訊。

一個 JWT 由三部分組成,中間用點 . 分隔:

  1. Header (標頭):包含 Token 的類型 (JWT) 與使用的加密演算法 (如 HS256)。
  2. Payload (內容):包含實際要傳輸的宣告 (Claims),例如使用者 ID 或權限。
  3. Signature (簽章):使用 Secret Key 與 Header、Payload 進行運算後的結果,用於驗證 Token 是否被竄改。

JWT 的核心優勢:無狀態 (Stateless)

因為 JWT 包含了簽章,伺服器收到 Token 後,只需使用本地的 Secret Key 驗證簽章即可確認 Token 的合法性,不需要查詢資料庫或快取 (如 Redis) 來核對 Session。這讓 API 擴展 (Scale) 變得非常容易。

安裝與設定 PyJWT

我們將使用 PyJWT 這個輕量且功能強大的函式庫。

# 推薦安裝包含密碼學支援的版本
pip install "PyJWT[cryptography]"

1. 產生 JWT Token

我們需要定義如何根據使用者資訊簽發 Token。

import jwt
from datetime import datetime, timedelta, timezone

# 實務上務必從環境變數讀取,切勿在程式碼中寫死!
SECRET_KEY = "your-super-secret-key-should-be-very-long-and-random"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()

    # 設定到期時間
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)

    # 加入 JWT 標準宣告 (Registered Claims)
    # exp: 到期時間; sub: 主體 (通常放使用者唯一 ID)
    to_encode.update({"exp": expire})

    # 使用 SECRET_KEY 進行簽名
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

2. 修改登入 Endpoint

驗證成功後,回傳符合 OAuth2 標準格式的 JWT。

@app.post("/token")
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
    # ... (帳號密碼驗證邏輯) ...
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=400, detail="帳密錯誤")

    # 產生 Access Token (sub 放入 username)
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user["username"]},
        expires_delta=access_token_expires
    )

    return {"access_token": access_token, "token_type": "bearer"}

3. 驗證與解析 JWT Token

這是最關鍵的一步:當客戶端帶上 Token 請求受保護的資源時,我們需要驗證它。

from jwt.exceptions import InvalidTokenError, ExpiredSignatureError

async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="無法驗證認證資訊",
        headers={"WWW-Authenticate": "Bearer"},
    )

    try:
        # 解碼並驗證簽章
        # PyJWT 會自動驗證 'exp' 是否過期
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception

    except ExpiredSignatureError:
        # 特別處理 Token 過期
        raise HTTPException(status_code=401, detail="Token 已過期,請重新登入")
    except InvalidTokenError:
        # 處理無效 Token (簽章錯誤、格式錯誤等)
        raise credentials_exception

    user = get_user(fake_users_db, username=username)
    if user is None:
        raise credentials_exception
    return user

安全最佳實踐 (Pro Tips)

  1. Secret Key 管理:在伺服器環境中使用 os.getenv("SECRET_KEY") 讀取,並確保開發與生產環境使用不同的 Key。
  2. Token 過期策略:不要設定過長的過期時間。Access Token 通常設為 15-60 分鐘。若需要長時間維持登入,應實作 Refresh Token 機制。
  3. Payload 內容:不要在 Payload 中存放機密資訊(如密碼、信用卡號),因為 Payload 只是經過 Base64 編碼,任何人都可以輕鬆解碼讀取。
  4. HTTPS:再一次強調,JWT 是在 Header 中傳輸的。如果你不使用 HTTPS,攻擊者可以輕易截獲 Token 並冒充使用者身分(這稱為 Token 劫持)。

總結

  1. Stateless:JWT 讓伺服器不需要儲存 Session 資訊。
  2. PyJWT:是 Python 社群中非常成熟且推薦的 JWT 處理函式庫。
  3. Claims:利用 subexp 等標準宣告來管理 Token 生命週期與身分。
  4. Security:配合 FastAPI 的 Depends 系統,能以極簡的程式碼建立安全的防護層。