什麼是 JWT (JSON Web Tokens)?
JWT (JSON Web Tokens) 是一種開放標準,用於在各方之間以 JSON 物件安全地傳輸資訊,透過數位簽章確保資料在傳輸過程中未被篡改,伺服器只需驗證即可,無需每次都向資料庫查核身分。
JWT (JSON Web Tokens) 基礎語法
- 安裝依賴套件:需要安裝 pip install "passlib[argon2]" pyjwt 處理加密簽章與處理密碼雜湊
- 定義 OAuth2:使用 OAuth2PasswordBearer 定義 Token 的獲取路徑(tokenUrl)
- jwt.encode:使用 SECRET_KEY 與演算法 (例如 HS256) 將使用者資訊與過期時間轉換成 Token
- jwt.decode:解碼 Token 並驗證簽章是否有效、是否過期,若失敗則拋出異常
- 保護路由:利用 Depends 將驗證函式注入到路由參數,確保請求必須攜帶有效 Token 才能執行
from datetime import datetime, timedelta, timezone
from typing import Annotated, Optional
import jwt
from jwt.exceptions import InvalidTokenError
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
from pydantic import BaseModel
# 在真實環境中,以下變數應由環境變數 (.env) 讀取,嚴禁寫死在程式碼中
SECRET_KEY = "my_super_secure_secret_key_for_demo_only"
ALGORITHM = "HS256"
TOKEN_EXPIRE_MINUTES = 30
app = FastAPI(title="JWT Auth Demo (PyJWT Version)")
# 密碼雜湊工具 (使用 argon2 演算法,需安裝 passlib[argon2])
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
# 定義 OAuth2 流程,'tokenUrl' 必須對應下方登入路由的 path
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
# 用於回應給前端的使用者資料 (不含密碼)
class UserPublic(BaseModel):
username: str
email: Optional[str] = None
# 資料庫內部的使用者結構 (包含雜湊密碼)
class UserPrivate(UserPublic):
password_hash: str
# Token 回應結構
class TokenResponse(BaseModel):
access_token: str
token_type: str
# --- 模擬資料庫 ---
MOCK_USER_DB = {
"admin": {
"username": "admin",
"email": "[email protected]",
"password_hash": pwd_context.hash("pass123"),
}
}
def create_jwt_token(data: dict, expires_delta: timedelta | None = None):
"""
生成帶有過期時間的 JWT 字串
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
# 使用 PyJWT 進行編碼
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> UserPrivate:
"""
驗證 Token 的依賴函式 (Dependency)。
解析 Token -> 檢查過期與正確性 -> 撈取使用者
"""
auth_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="無法驗證憑證",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# decode 會自動驗證 exp (過期時間) 和簽章
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise auth_exception
except InvalidTokenError:
raise auth_exception
# 確認使用者是否存在於資料庫
user_data = MOCK_USER_DB.get(username)
if user_data is None:
raise auth_exception
return UserPrivate(**user_data)
@app.post("/auth/login", response_model=TokenResponse)
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
):
"""
登入路由:接收 form-data (username, password),回傳 JWT。
"""
user = MOCK_USER_DB.get(form_data.username)
# 驗證帳號是否存在 以及 密碼是否匹配
if not user or not pwd_context.verify(form_data.password, user["password_hash"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="帳號或密碼錯誤",
headers={"WWW-Authenticate": "Bearer"},
)
# 製作 Token
access_token_expires = timedelta(minutes=TOKEN_EXPIRE_MINUTES)
access_token = create_jwt_token(
data={"sub": user["username"]},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me", response_model=UserPublic)
async def read_current_user(
current_user: Annotated[UserPrivate, Depends(get_current_user)]
):
"""
受保護的路由:必須攜帶有效 Token 才能訪問。
Response Model 會自動過濾掉 UserPrivate 中的 password_hash
"""
return current_user



















