## 1. 「已登入卻認不出我?」(問題的起點) {#sec-ffb74987f051} OAuth2、JWT、Session 認證… 認證方式多種多樣,對大多數情況已足夠。我的經驗也是如此。 * 為自製的 Email 客戶端或 ChatGPT 的 MyGPT 加上 **OAuth2** 時,彷彿體驗到了真正的使用者體驗 * 完全由 Django 伺服器提供的 Web App,**Session 認證** 是最佳選擇! * 前後端分離的架構下,**JWT** 最為乾淨利落 但是,這些組合在某一刻會同時崩潰。崩潰的關鍵在 **非同步工作 (Celery)**。使用者點擊按鈕時,後端不直接處理,而是把任務交給遠端的 AI 計算伺服器或 Worker。此時 Worker 會說: > "嗯… 收到請求了,但這是替誰執行的?request.user 不存在啊。" ![機器人 Worker 手持 API Key 送信的圖示](/media/editor_temp/6/b63803ff-21ba-4f1b-a5af-47c8ca0fdd25.png) ## 2. 問題在「後端 ↔ 後端」+「非同步 Worker (Celery)」 {#sec-a7eab2678145} 我最終決定導入 API Key,根本原因是 **後端服務之間的通訊** 中 **Celery Worker 介入** 的架構。 1. 使用者發送 Web 請求 2. 後端將「任務」放入佇列 3. Celery Worker 消費佇列,**向某個後端/計算伺服器發送非同步請求** 在這個流程中,最痛的地方是: * Worker 沒有 **request.user** * 也沒有 Session(因為不是瀏覽器) * JWT 更是尷尬(誰在何處、如何管理與傳遞 token) * OAuth2 完全依賴「使用者互動」的流程,根本不可行 當 JWT 與 Session 失效時,唯一剩下的問題被濃縮為: > 「計算伺服器如何得知這個工作屬於哪個客戶(租戶/使用者)?」 ## 3. 在 Worker 世界裡,先要「識別」再談「認證」 {#sec-77f5863a0a93} Web 請求的流程是「認證=登入」且「登入=使用者」自然相連。 但 Worker 不是人,而是自動借用 CPU 執行任務的應用程式,因而在「認證」之前必須先「識別」: * 任務必須以 A 客戶的資料執行 * 結果必須存回 A 客戶的資源 * 計費/權限/配額也必須以 A 客戶為基礎扣除 如果硬要把 JWT 或 Session 塞進來,會產生 token 發行、保存、傳遞、過期、重新發行等繁雜設計,甚至會產生「即使是我們自己的客戶,但他不是在瀏覽器操作,後端卻要去取得 JWT?」的強烈違和感。這種想法我立刻棄用。 ## 4. 解決方案:API Key 在此環節既簡單又有力 {#sec-2110738d8481} 於是我採用了 API Key,問題瞬間迎刃而解。 * Worker 在內部請求時,只需在 **Header** 加上一行即可同時完成認證與識別 * 伺服器看到這把鍵後,立即 **映射到對應的使用者/客戶** * 鍵的撤銷或輪換也變得非常清晰 例如,伺服器向計算伺服器發送非同步請求的樣子如下: ```http POST /v1/ai/jobs Authorization: Api-Key Content-Type: application/json { "job_id": "...", "payload": {...} } ``` Worker 不需要 `request.user`,只要把上述請求發出去,接收端即可依照說明使用 API Key 來辨識使用者。 ## 5. 關鍵改進:將 API Key 與 USER 直接綁定,運維更輕鬆 {#sec-762242363071} 我特別滿意的地方在於此。 傳統的 `rest_framework_api_key` 僅提供鍵本身,而我的需求是 **「鍵 ↔ 使用者(客戶) 的結合」**。因此我繼承 `AbstractAPIKey`,建立 `CustomAPIKey`,並以 **FK** 連結到 `AUTH_USER` 模型。 結果相當令人滿意。 ```python from django.conf import settings from rest_framework_api_key.models import AbstractAPIKey from django.db import models class CustomAPIKey(AbstractAPIKey): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="api_keys" ) is_test = models.BooleanField(default=False) # 區分測試/正式鍵 ``` 如此一來,除了「認證」之外,還能開啟一系列 **運營功能**。 ## 6. 為每位使用者自動發放鍵帶來的運營收益 {#sec-d180d12a94f2} 在使用者註冊時自動為其生成一把鍵,讓以下工作變得非常便利。 ### 1) 易於管理有效性 {#sec-bfe883fb0c50} * 可直接查詢、停用或刪除特定使用者的鍵 * 使用者刪除時,FK cascade 會自動清除相關鍵 ### 2) 鍵輪換更簡單 {#sec-7f4bccda836d} * 發現鍵可能外洩時,只需發新鍵、廢除舊鍵即可 * 支援多把鍵同時存在,實現 **無停機替換** ### 3) 計費/配額/權限以使用者為單位 {#sec-1081bea2890b} * 不必再額外推斷「這把 API Key 屬於誰」 * 直接以使用者為基礎套用計費與限制 ### 4) 同一使用者可擁有多把鍵 {#sec-1bc8fa524379} `is_test` 旗標在此派上用場。因為 API Key 是 **FK** 而非 OneToOne,單一使用者可以依用途分配多把鍵。 * 同時持有 **測試環境鍵** 與 **正式環境鍵** * 開發/運營流程能輕鬆分離 * 日誌與監控也能區分「測試流量」與「真實流量」 ## 7. 認證方式不是優劣,而是「情境武器」 {#sec-f73d5faf76f6} 總結我的「情境最佳組合」如下: * **OAuth2**:適合外部服務/客戶端整合,需使用者同意的流程 * **Session 認證**:單一 Django 網站開發速度最快、最簡潔 * **JWT**:前後端分離、行動端、SPA 等多端平衡最佳 * **API Key**:後端 ↔ 後端、自動化、Worker、批次等「非使用者請求」情境最為便利 尤其在 Celery Worker 介入時,試圖用「登入基礎認證」統一全局只會增加複雜度。此時 API Key 成為最乾淨的出路。 ## 8. 結語 {#sec-d746e56908e8} 人(瀏覽器/APP)自然使用 Session/JWT/OAuth2 處理認證。 但 Worker 不是人,而是程式,它必須能「辨識」是哪個客戶的工作。 我轉向 API Key 並非因為安全議題,而是因為在那個環節它是最簡單、最直接的解法。把鍵與使用者綁定後,鍵的管理不再是工具,而是 **運營槓桿**。 讀者有在使用 API Key 的經驗嗎?本文分享的做法僅是一種便利的示例,期待能為你帶來靈感。 --- **相關文章** - [Django/DRF 使用 HMAC 簽名保護 Server‑to‑Server 請求完整性](/ko/whitedec/2025/12/9/django-drf-hmac-signature-server-to-server-integrity/) - [React RCE 事件的教訓:HMAC 簽名、金鑰輪換與 Zero Trust 必要性](/ko/whitedec/2025/12/8/react-rce-lesson-hmac-key-rotation-zero-trust/)