FastAPI 課程 – 安全性(Security)
主題:API Key 認證
簡介
在現代的微服務與前後端分離的架構中,API 安全性 是不可或缺的基礎。即使是最簡單的資料查詢,也可能因為缺乏適當的驗證機制而被未授權的使用者濫用,導致資源耗盡、資料外洩甚至服務中斷。
API Key(應用程式金鑰)是一種輕量級的認證方式,特別適合於 內部服務、第三方合作夥伴 或 行動/IoT 裝置 等情境。它的實作相對簡單,同時能提供足夠的保護,讓開發者在不需要完整的 OAuth2 流程時,也能快速為 FastAPI 應用加上驗證層。
本文將從 概念說明、實作範例、常見陷阱與最佳實踐,到 真實案例,一步步帶你掌握在 FastAPI 中使用 API Key 進行認證的完整流程,適合剛入門或已具備一定 Python/FastAPI 基礎的開發者閱讀。
核心概念
1. API Key 是什麼?
API Key 本質上是一串 唯一且隨機的字串(例如 abcd1234efgh5678),由服務提供者產生並發給客戶端。客戶端在每次呼叫 API 時,將此金鑰放在 HTTP Header、Query Parameter 或 Cookie 中,伺服器端則依據金鑰的存在與有效性,決定是否允許存取資源。
重點:API Key 只負責 身份驗證(Authentication),不會說明使用者的權限(Authorization),若需要細緻的權限控制,仍需結合角色(role)或 scope 機制。
2. FastAPI 提供的工具
FastAPI 內建 fastapi.security.APIKeyHeader、APIKeyQuery、APIKeyCookie 三種依賴(dependency)類別,分別對應於不同的傳遞方式。這些類別會自動產生 OpenAPI(Swagger)文件,讓前端或第三方開發者清楚知道如何提供金鑰。
3. 設計 API Key 的存取方式
| 傳遞方式 | 範例 Header | 範例 Query | 範例 Cookie |
|---|---|---|---|
| Header | X-API-Key: <key> |
- | - |
| Query | - | /items?api_key=<key> |
- |
| Cookie | - | - | api_key=<key> |
建議:在大多數情況下,Header 是最安全且最符合 REST 風格的做法,因為 Query 會被瀏覽器快取、日誌或代理伺服器記錄,增加金鑰外洩風險。
4. 金鑰的產生與儲存
- 產生:使用
secrets.token_urlsafe(32)產生足夠長度且不可預測的金鑰。 - 儲存:將金鑰與相關資訊(如擁有者、建立時間、失效日期)存入資料庫或安全的密鑰管理服務(KMS)。絕對不要將金鑰硬編碼在程式碼中。
程式碼範例
以下示範三種不同的傳遞方式,以及如何在 FastAPI 中建立可重用的驗證依賴。所有範例均使用 Python 3.9+ 與 FastAPI 0.110+。
1. 基本的 Header 方式
# file: main_header.py
from fastapi import FastAPI, Depends, HTTPException, Security, status
from fastapi.security import APIKeyHeader
from typing import List
app = FastAPI()
# 1️⃣ 宣告 Header 名稱
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
# 2️⃣ 假設的金鑰資料庫(實務上請改用真實 DB)
FAKE_DB = {
"abcd1234efgh5678": {"owner": "client_a"},
"ijkl9012mnop3456": {"owner": "client_b"},
}
def get_api_key(
api_key: str = Security(api_key_header)
):
"""
驗證 API Key 是否存在於 FAKE_DB。
若無或錯誤,拋出 401 錯誤。
"""
if not api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing API Key",
)
if api_key not in FAKE_DB:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid API Key",
)
return FAKE_DB[api_key] # 回傳與金鑰相關的資訊
@app.get("/protected")
def protected_route(user: dict = Depends(get_api_key)):
"""
只有持有正確 API Key 的請求才能進入此路由。
"""
return {"message": f"Hello, {user['owner']}! You are authorized."}
說明
APIKeyHeader會自動在 OpenAPI 文件中產生X-API-Key欄位。auto_error=False讓我們自行決定錯誤訊息的內容。Depends(get_api_key)讓protected_route只在驗證成功時才被呼叫。
2. Query Parameter 方式(適合測試或簡易腳本)
# file: main_query.py
from fastapi import FastAPI, Depends, HTTPException, Security, status
from fastapi.security import APIKeyQuery
app = FastAPI()
api_key_query = APIKeyQuery(name="api_key", auto_error=False)
# 假資料庫
FAKE_DB = {"secret123": {"owner": "script_user"}}
def get_api_key_from_query(
api_key: str = Security(api_key_query)
):
if not api_key:
raise HTTPException(status_code=401, detail="API key missing")
if api_key not in FAKE_DB:
raise HTTPException(status_code=403, detail="Invalid API key")
return FAKE_DB[api_key]
@app.get("/items")
def read_items(user: dict = Depends(get_api_key_from_query)):
return {"items": ["apple", "banana"], "owner": user["owner"]}
實務提示:若必須使用 Query,務必在 HTTPS 下傳輸,並在伺服器端設定 Cache-Control: no-store,減少金鑰被快取的風險。
3. 結合 Cookie 與 Header 的雙重驗證
# file: main_cookie.py
from fastapi import FastAPI, Depends, HTTPException, Security, status, Response
from fastapi.security import APIKeyHeader, APIKeyCookie
app = FastAPI()
HEADER_NAME = "X-API-Key"
COOKIE_NAME = "api_key"
api_key_header = APIKeyHeader(name=HEADER_NAME, auto_error=False)
api_key_cookie = APIKeyCookie(name=COOKIE_NAME, auto_error=False)
FAKE_DB = {"cookie_key_001": {"owner": "web_user"}}
def verify_api_key(
header_key: str = Security(api_key_header),
cookie_key: str = Security(api_key_cookie)
):
"""
允許客戶端同時提供 Header 或 Cookie 任一種金鑰。
若兩者皆提供,Header 會優先使用。
"""
key = header_key or cookie_key
if not key:
raise HTTPException(status_code=401, detail="API key missing")
if key not in FAKE_DB:
raise HTTPException(status_code=403, detail="Invalid API key")
return FAKE_DB[key]
@app.get("/dashboard")
def dashboard(user: dict = Depends(verify_api_key)):
return {"message": f"Welcome {user['owner']} to the dashboard"}
@app.post("/login")
def login(response: Response):
"""
假設的登入端點,回傳一個 Set-Cookie。
真實環境應先驗證使用者身份,再產生對應的 API Key。
"""
api_key = "cookie_key_001"
response.set_cookie(key=COOKIE_NAME, value=api_key, httponly=True, secure=True)
return {"msg": "Logged in, cookie set"}
安全建議
httponly=True防止 JavaScript 讀取 Cookie,降低 XSS 攻擊風險。secure=True確保 Cookie 只在 HTTPS 連線下傳送。
4. 統一的依賴與自訂例外處理(進階)
# file: security.py
from fastapi import HTTPException, Request, status
from fastapi.security import APIKeyHeader
from starlette.responses import JSONResponse
API_KEY_NAME = "X-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
# 假設的金鑰儲存與查詢服務
class APIKeyStore:
@staticmethod
def verify(key: str) -> dict | None:
# 這裡可以改成 DB 查詢或呼叫外部 KMS
valid_keys = {"super_key_123": {"owner": "admin"}}
return valid_keys.get(key)
async def api_key_auth(request: Request):
api_key = await api_key_header(request)
if not api_key:
raise HTTPException(status_code=401, detail="Missing API Key")
user = APIKeyStore.verify(api_key)
if not user:
raise HTTPException(status_code=403, detail="Invalid API Key")
return user
# 自訂例外處理器,讓錯誤回應更友善
def api_key_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"error": exc.detail, "type": "api_key_error"},
)
# file: main_app.py
from fastapi import FastAPI, Depends
from security import api_key_auth, api_key_exception_handler
app = FastAPI()
app.add_exception_handler(HTTPException, api_key_exception_handler)
@app.get("/admin")
def admin_panel(user: dict = Depends(api_key_auth)):
return {"msg": f"Hello {user['owner']}, welcome to admin panel"}
說明
- 透過
api_key_auth可在多個路由間共用同一套驗證邏輯。- 自訂例外處理器讓前端開發者可以根據
type欄位快速判斷錯誤來源。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 建議的解決方案 |
|---|---|---|
| 金鑰硬編碼在程式碼 | 金鑰洩漏、無法快速撤銷 | 使用環境變數、.env 或 Secret Manager;金鑰變更時只需更新配置。 |
| 將金鑰放在 URL Query | 被瀏覽器、代理、日誌記錄,增加外洩機會 | 優先使用 Header;若必須使用 Query,務必在 HTTPS 之下,且在伺服器端設定 Cache-Control: no-store。 |
| 金鑰未設定過期時間 | 長期有效的金鑰成為攻擊者的永久入口 | 為金鑰加入 TTL(Time‑to‑Live),定期輪換;可在資料庫中儲存 expires_at 欄位。 |
| 未對金鑰做速率限制 | 暴力猜測或 DoS 攻擊 | 搭配 Rate Limiting(如 slowapi、starlette-limiter)或 API Gateway(AWS API GW、Kong)做流量管控。 |
| 回應中回傳過於詳細的錯誤訊息 | 攻擊者可根據訊息推測金鑰驗證流程 | 統一錯誤格式,僅返回「未授權」或「禁止」訊息;把詳細日誌寫入伺服器端。 |
| 未使用 HTTPS | 金鑰在傳輸過程被截取(MITM) | 必須在生產環境部署 TLS;開發環境可使用 uvicorn --reload --host 0.0.0.0 --port 8000 搭配自簽憑證。 |
最佳實踐清單
- 金鑰長度:至少 32 位元(Base64)以上,使用
secrets模組產生。 - 儲存方式:Hash(如 SHA‑256)後再存入資料庫,避免明文保存。
- 授權層級:金鑰本身只驗證身份,若需要 RBAC,在
user物件中加入role、scopes,再於路由內做權限檢查。 - 金鑰撤銷:提供管理介面或 API 能即時將金鑰標記為 inactive,並在驗證函式中檢查此狀態。
- 監控與日誌:記錄每次金鑰驗證的時間、IP、路徑,並使用 SIEM 監控異常模式。
實際應用場景
| 場景 | 為什麼選擇 API Key | 實作要點 |
|---|---|---|
| 內部微服務間的同步呼叫 | 服務間信任關係明確,金鑰管理成本低 | 在 Kubernetes secret 中注入金鑰,使用 Header X-API-Key。 |
| 第三方合作夥伴的資料匯入 | 合作方不需要完整的 OAuth 流程,僅需授權特定端點 | 為每個合作夥伴生成獨立金鑰,並在資料庫中設定 partner_id、expires_at。 |
| 行動 App 與後端 API | 手機端無法方便完成 OAuth2 授權流程 | 在 App 首次安裝時向認證服務取得一次性金鑰,之後以 Header 傳送。 |
| IoT 裝置上報感測資料 | 裝置資源有限,僅能使用簡單的金鑰驗證 | 金鑰寫入裝置韌體,使用 HTTPS POST 並在 Cloud 端驗證。 |
| 公共 API(如天氣、匯率) | 需要追蹤使用量、限制濫用 | 為每個開發者發放 API Key,結合 Rate Limiting 與 Quota 機制。 |
案例說明:
假設公司提供「即時庫存」的公共 API,外部合作夥伴每日只能查詢 10,000 筆。開發團隊在 FastAPI 中使用APIKeyHeader取得金鑰,並於依賴函式內同時查詢 Redis 中的計數器,若超過配額則回傳429 Too Many Requests。這樣既能保護系統,又能提供易於整合的介面。
總結
- API Key 是在 FastAPI 中實作輕量級認證的首選工具,特別適合 內部服務、合作夥伴或資源受限的裝置。
- 透過
APIKeyHeader、APIKeyQuery、APIKeyCookie三種內建依賴,我們可以快速產生符合 OpenAPI 規範的文件,同時保有高度可自訂的驗證邏輯。 - 實務上必須注意 金鑰的產生、儲存、過期與撤銷,以及 HTTPS、速率限制、錯誤訊息隱蔽 等安全要點,以避免金鑰外洩或被濫用。
- 只要遵循 最佳實踐清單,結合 日誌、監控與金鑰輪換,就能在開發速度與安全性之間取得良好的平衡。
希望透過本篇文章,你能在自己的 FastAPI 專案中自信地加入 API Key 認證,為服務打造堅實的第一道防線。祝開發順利,API 安全永續! 🚀