FastAPI JWT Token 實作登入機制
上一章我們實作了登入流程並使用密碼雜湊來保護帳號。然而,當時回傳的 Token 只是一個簡單的字串 (username),這在實務上無法滿足安全性與效能需求。
現代 Web API 最主流的解決方案是 JWT (JSON Web Token)。
什麼是 JWT?
JWT 是一個開放標準 (RFC 7519),它定義了一種緊湊且自包含的方式,用於在各方之間以 JSON 物件安全地傳輸資訊。
一個 JWT 由三部分組成,中間用點 . 分隔:
- Header (標頭):包含 Token 的類型 (JWT) 與使用的加密演算法 (如 HS256)。
- Payload (內容):包含實際要傳輸的宣告 (Claims),例如使用者 ID 或權限。
- 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)
- Secret Key 管理:在伺服器環境中使用
os.getenv("SECRET_KEY")讀取,並確保開發與生產環境使用不同的 Key。 - Token 過期策略:不要設定過長的過期時間。Access Token 通常設為 15-60 分鐘。若需要長時間維持登入,應實作 Refresh Token 機制。
- Payload 內容:不要在 Payload 中存放機密資訊(如密碼、信用卡號),因為 Payload 只是經過 Base64 編碼,任何人都可以輕鬆解碼讀取。
- HTTPS:再一次強調,JWT 是在 Header 中傳輸的。如果你不使用 HTTPS,攻擊者可以輕易截獲 Token 並冒充使用者身分(這稱為 Token 劫持)。
總結
- Stateless:JWT 讓伺服器不需要儲存 Session 資訊。
- PyJWT:是 Python 社群中非常成熟且推薦的 JWT 處理函式庫。
- Claims:利用
sub與exp等標準宣告來管理 Token 生命週期與身分。 - Security:配合 FastAPI 的
Depends系統,能以極簡的程式碼建立安全的防護層。