問題背景
在開發 FastAPI + Supabase 專案時,需要在後端驗證前端傳來的 JWT token。最初AI,使用了 Legacy JWT Secret 搭配 HS256 演算法驗證:
# 原本的寫法(使用 Legacy JWT Secret)
import jwt
payload = jwt.decode(
token,
SUPABASE_JWT_SECRET, # Legacy JWT Secret
algorithms=["HS256"],
audience="authenticated",
)
Code language: PHP (php)問題原因:兩種 Key 的本質差異
Supabase 有兩套 JWT 簽署機制:
| 類型 | 演算法 | 驗證方式 | 安全性 |
|---|---|---|---|
| Legacy JWT Secret | HS256(對稱加密) | 需要共享密鑰 | 較低 |
| JWT Signing Keys | ES256/RS256(非對稱加密) | 使用 JWKS 公鑰驗證 | 較高 |
Legacy JWT Secret (HS256)
- 對稱加密:簽署和驗證使用同一把密鑰
- 需要把 Secret 存在後端環境變數
- 如果 Secret 洩漏,攻擊者可以偽造 token
JWT Signing Keys (ES256/RS256)
- 非對稱加密:私鑰簽署、公鑰驗證
- 後端只需要公鑰(透過 JWKS endpoint 取得)
- 即使公鑰曝露也無法偽造 token
解決方案:改用 JWKS 驗證
Supabase 提供 JWKS (JSON Web Key Set) endpoint,可以取得公鑰來驗證 token:
https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
Code language: HTML, XML (xml)步驟 1:安裝 python-jose
# 使用 uv
uv add python-jose[cryptography]
# 或使用 pip
pip install python-jose[cryptography]
Code language: CSS (css)步驟 2:修改環境變數
# .env(移除 JWT Secret,改用 Project Reference)
SUPABASE_PROJECT_REF=你的project-ref
Code language: PHP (php)步驟 3:修改驗證程式碼
"""JWT 驗證(使用 JWKS 非對稱驗證)"""
import os
from functools import lru_cache
import requests
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
SUPABASE_PROJECT_REF = os.environ.get("SUPABASE_PROJECT_REF", "")
security = HTTPBearer()
@lru_cache(maxsize=1)
def get_jwks() -> dict:
"""
取得並快取 Supabase JWKS。
使用 lru_cache 避免每次請求都重新取得。
"""
if not SUPABASE_PROJECT_REF:
raise ValueError("SUPABASE_PROJECT_REF 未設定")
jwks_url = f"https://{SUPABASE_PROJECT_REF}.supabase.co/auth/v1/.well-known/jwks.json"
response = requests.get(jwks_url, timeout=10)
response.raise_for_status()
return response.json()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> dict:
"""從 JWT token 解析並驗證用戶"""
token = credentials.credentials
try:
jwks = get_jwks()
# 注意:演算法要根據你的 JWKS 設定
# 可以先 curl JWKS endpoint 確認是 ES256 還是 RS256
payload = jwt.decode(
token,
jwks,
algorithms=["ES256"], # 或 RS256,視你的設定
audience="authenticated",
issuer=f"https://{SUPABASE_PROJECT_REF}.supabase.co/auth/v1",
)
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"無效的 Token: {str(e)}",
)
return {
"email": payload.get("email"),
"sub": payload.get("sub"),
}
Code language: PHP (php)步驟 4:確認演算法類型
先檢查你的 JWKS 使用什麼演算法:
curl https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
Code language: HTML, XML (xml)回應範例:
{
"keys": [{
"alg": "ES256", // <-- 看這裡!ES256 或 RS256
"kty": "EC",
"kid": "77327ed6-ecd7-4e2e-b5f8-edcc0320b6c2",
...
}]
}
Code language: JavaScript (javascript)根據 alg 欄位設定 algorithms 參數。
常見錯誤排除
錯誤 1:Signature verification failed
原因:演算法設定錯誤(例如 JWKS 是 ES256,但程式碼寫 RS256)
解決:確認 JWKS 的 alg 欄位,對應修改程式碼
錯誤 2:Token 缺少必要欄位
原因:token 不是 Supabase Auth 發出的,或是 anon key 產生的
解決:確保前端使用 supabase.auth.getSession() 取得的 access_token
錯誤 3:JWKS 請求失敗
原因:網路問題或 Project Reference 錯誤
解決:
- 確認
SUPABASE_PROJECT_REF正確 - 在容器環境中確認 DNS 解析正常
為什麼要改?
- 更安全:非對稱加密,公鑰曝露也無法偽造 token
- 官方推薦:Supabase 正在從 Legacy keys 過渡到新的 API keys 系統
- 自動輪換:JWKS 支援 key rotation,提升安全性
- 無需共享密鑰:後端只需要 Project Reference,不需要存放敏感的 Secret
結論
如果你的 Supabase 專案使用的是 JWT Signing Keys(可以從 Dashboard 的 API Keys 頁面確認),就必須使用 JWKS 方式驗證,而不是 Legacy JWT Secret。
這個改動雖然需要修改程式碼,但帶來更好的安全性和符合官方的發展方向。