本文 AI 產出,尚未審核

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 搭配自簽憑證。

最佳實踐清單

  1. 金鑰長度:至少 32 位元(Base64)以上,使用 secrets 模組產生。
  2. 儲存方式:Hash(如 SHA‑256)後再存入資料庫,避免明文保存。
  3. 授權層級:金鑰本身只驗證身份,若需要 RBAC,在 user 物件中加入 role、scopes,再於路由內做權限檢查。
  4. 金鑰撤銷:提供管理介面或 API 能即時將金鑰標記為 inactive,並在驗證函式中檢查此狀態。
  5. 監控與日誌:記錄每次金鑰驗證的時間、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 安全永續! 🚀