本文 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 ParameterCookie 中,伺服器端則依據金鑰的存在與有效性,決定是否允許存取資源。

重點:API Key 只負責 身份驗證(Authentication),不會說明使用者的權限(Authorization),若需要細緻的權限控制,仍需結合角色(role)或 scope 機制。

2. FastAPI 提供的工具

FastAPI 內建 fastapi.security.APIKeyHeaderAPIKeyQueryAPIKeyCookie 三種依賴(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 欄位快速判斷錯誤來源。

常見陷阱與最佳實踐

陷阱 可能的後果 建議的解決方案
金鑰硬編碼在程式碼 金鑰洩漏、無法快速撤銷 使用環境變數、.envSecret Manager;金鑰變更時只需更新配置。
將金鑰放在 URL Query 被瀏覽器、代理、日誌記錄,增加外洩機會 優先使用 Header;若必須使用 Query,務必在 HTTPS 之下,且在伺服器端設定 Cache-Control: no-store
金鑰未設定過期時間 長期有效的金鑰成為攻擊者的永久入口 為金鑰加入 TTL(Time‑to‑Live),定期輪換;可在資料庫中儲存 expires_at 欄位。
未對金鑰做速率限制 暴力猜測或 DoS 攻擊 搭配 Rate Limiting(如 slowapistarlette-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 物件中加入 rolescopes,再於路由內做權限檢查。
  4. 金鑰撤銷:提供管理介面或 API 能即時將金鑰標記為 inactive,並在驗證函式中檢查此狀態。
  5. 監控與日誌:記錄每次金鑰驗證的時間、IP、路徑,並使用 SIEM 監控異常模式。

實際應用場景

場景 為什麼選擇 API Key 實作要點
內部微服務間的同步呼叫 服務間信任關係明確,金鑰管理成本低 在 Kubernetes secret 中注入金鑰,使用 Header X-API-Key
第三方合作夥伴的資料匯入 合作方不需要完整的 OAuth 流程,僅需授權特定端點 為每個合作夥伴生成獨立金鑰,並在資料庫中設定 partner_idexpires_at
行動 App 與後端 API 手機端無法方便完成 OAuth2 授權流程 在 App 首次安裝時向認證服務取得一次性金鑰,之後以 Header 傳送。
IoT 裝置上報感測資料 裝置資源有限,僅能使用簡單的金鑰驗證 金鑰寫入裝置韌體,使用 HTTPS POST 並在 Cloud 端驗證。
公共 API(如天氣、匯率) 需要追蹤使用量、限制濫用 為每個開發者發放 API Key,結合 Rate LimitingQuota 機制。

案例說明
假設公司提供「即時庫存」的公共 API,外部合作夥伴每日只能查詢 10,000 筆。開發團隊在 FastAPI 中使用 APIKeyHeader 取得金鑰,並於依賴函式內同時查詢 Redis 中的計數器,若超過配額則回傳 429 Too Many Requests。這樣既能保護系統,又能提供易於整合的介面。


總結

  • API Key 是在 FastAPI 中實作輕量級認證的首選工具,特別適合 內部服務、合作夥伴或資源受限的裝置
  • 透過 APIKeyHeaderAPIKeyQueryAPIKeyCookie 三種內建依賴,我們可以快速產生符合 OpenAPI 規範的文件,同時保有高度可自訂的驗證邏輯。
  • 實務上必須注意 金鑰的產生、儲存、過期與撤銷,以及 HTTPS、速率限制、錯誤訊息隱蔽 等安全要點,以避免金鑰外洩或被濫用。
  • 只要遵循 最佳實踐清單,結合 日誌、監控與金鑰輪換,就能在開發速度與安全性之間取得良好的平衡。

希望透過本篇文章,你能在自己的 FastAPI 專案中自信地加入 API Key 認證,為服務打造堅實的第一道防線。祝開發順利,API 安全永續! 🚀