# DRF Throttling(請求限制)全攻略:為何需要、如何設定、如何應用、如何自訂 在 nginx 反向代理中使用 `limit_req` 來「入口截流」雖然有效,但當你想為不同的視圖/動作設定不同的限制(例如:登入每分鐘 5 次、上傳每天 20 次、查詢 API 每人 1000 次)時,僅靠伺服器或基礎設施的設定往往難以滿足。DRF 的 throttling 允許你在應用層面針對「端點特性」建立限制,這是「必備基礎」之一。 --- ## 為什麼需要 Throttling DRF 的 throttling 與「權限(permission)」類似,都是決定是否允許請求,但差異在於:權限是永久性的,而 throttling 是臨時的請求頻率限制。官方文件將 throttling 描述為「控制客戶端對 API 的請求速率的臨時狀態」。 實際需求可歸納為: * **防止濫用/攻擊**:暴力破解登入、垃圾訊息、爬蟲、簡單 DoS 等 * **成本/資源保護**:上傳、外部 API 呼叫、重負載查詢、生成式 AI 呼叫等「昂貴」端點 * **公平使用**:避免單一使用者/金鑰佔用資源 * **政策編碼化**:將「此 API 每分鐘 N 次」等規則以程式碼方式管理,而非僅靠基礎設施 重要的是,throttling 可以全域(global)或針對特定視圖/動作(view/action)設定,這正是 nginx 只能做「入口截流」時的補充。 --- ## DRF Throttling 的工作原理(核心概念) ### 1) Rate 字串 DRF 以類似 `"100/day"`、`"60/min"` 的格式設定限制。 ### 2) 「誰」被限制(客戶端識別) * `UserRateThrottle`:已驗證使用者以 **user id** 為基準,未驗證則以 **IP** 為基準 * `AnonRateThrottle`:僅對未驗證(anonymous)請求以 IP 為基準 * `ScopedRateThrottle`:可為「此視圖是 uploads 範圍」等 **scope** 單位設定政策 IP 會透過 `X-Forwarded-For` 或 `REMOTE_ADDR` 取得,若位於代理後面,`NUM_PROXIES` 設定尤為重要。 ### 3) 狀態儲存使用 Cache DRF 的預設 throttling 會將計數存於 **Django cache backend**。單一進程/單一伺服器可使用 `LocMemCache`,但多工作者或多副本環境則必須使用 Redis 等共享 Cache。 --- ## 全域(Global)設定:最快速的起步 ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "100/day", "user": "1000/day", }, } ``` 這樣即可為整個 API 應用「基礎政策」。 當請求被限制時,DRF 會回傳 **HTTP 429(Too Many Requests)**。 --- ## 如何在視圖中設定:端點別差異化 ### 1) 類別視圖(APIView) ```python from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.throttling import UserRateThrottle class ExpensiveView(APIView): throttle_classes = [UserRateThrottle] def get(self, request): return Response({"ok": True}) ``` 文檔亦建議使用 `throttle_classes` 於視圖層級設定。 ### 2) 函式視圖(@api_view) ```python from rest_framework.decorators import api_view, throttle_classes from rest_framework.throttling import UserRateThrottle from rest_framework.response import Response @api_view(["GET"]) @throttle_classes([UserRateThrottle]) def ping(request): return Response({"pong": True}) ``` ### 3) ViewSet 的特定 action(@action) ```python from rest_framework.decorators import action from rest_framework.throttling import UserRateThrottle from rest_framework.viewsets import ViewSet from rest_framework.response import Response class ItemViewSet(ViewSet): @action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle]) def purchase(self, request, pk=None): return Response({"purchased": pk}) ``` action 內設定的 throttling 會覆蓋 ViewSet 層級的設定。 --- ## 使用 ScopedRateThrottle 建立「視圖特性」政策(強烈推薦) Scoped throttling 允許你以「有意義的名稱(scope)」區分政策,例如:`uploads`、`login`,使運營更清晰。 ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.ScopedRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "login": "5/min", "uploads": "20/day", "search": "60/min", }, } ``` 在視圖中只需宣告 `throttle_scope`: ```python from rest_framework.views import APIView from rest_framework.response import Response class LoginView(APIView): throttle_scope = "login" def post(self, request): return Response({"ok": True}) ``` DRF 會為帶有 `throttle_scope` 的視圖使用 ScopedRateThrottle,並以「scope + 使用者 id 或 IP」作為唯一鍵計數。 --- ## 自訂 Throttle:關鍵在「如何抓取鍵」 雖然內建 throttling 已能解決大部分需求,但實務上常見的需求包括: * 「登入以 IP + 使用者名稱組合限制」 * 「以 API Key 限制」 * 「以特定標頭/租戶/組織單位限制」 * 「使用非預設 Cache(如 Redis cluster)」 ### 1) 最常見方式:繼承 SimpleRateThrottle 只要正確實作 `get_cache_key()`,即可自由決定「以什麼為基準」限制。 ```python from rest_framework.throttling import SimpleRateThrottle class LoginBurstThrottle(SimpleRateThrottle): scope = "login" def get_cache_key(self, request, view): username = (request.data.get("username") or "").lower().strip() ident = self.get_ident(request) # IP 基礎識別(受代理設定影響) if not username: return None # 若無使用者名稱則不限制(可自行調整) return f"throttle_login:{ident}:{username}" ``` 並於 settings 註冊: ```python REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "path.to.LoginBurstThrottle", ], "DEFAULT_THROTTLE_RATES": { "login": "5/min", }, } ``` ### 2) 想改用其他 Cache(cache attribute) ```python from django.core.cache import caches from rest_framework.throttling import AnonRateThrottle class CustomCacheAnonThrottle(AnonRateThrottle): cache = caches["alternate"] ``` --- ## 部署前必知! ### 1) 代理環境下 IP 取得問題 IP 透過 `X-Forwarded-For`/`REMOTE_ADDR` 取得,若代理後未正確設定 `NUM_PROXIES`,所有使用者可能被視為同一 IP。 ### 2) LocMemCache 在多工作者/多伺服器環境下脆弱 本地 Cache 會在每個工作者/伺服器間獨立計數,導致 throttling 無法正確工作。建議使用 Redis 等共享 Cache。 ### 3) 同步競爭條件(race condition) 內建 throttling 在高併發下可能允許「多於 N 次」請求。若需「精確 N 次」的關鍵操作(如支付、優惠券),建議使用 Redis INCR + EXPIRE 等原子操作自行實作。 ### 4) 客戶端友好:429 與 Retry-After DRF 會回傳 429,若實作 `wait()` 方法,可在回應中加入 `Retry-After` 標頭,告知客戶何時可再次嘗試。 --- ## 總結:nginx 與 DRF throttling,兩者皆可用 ![機器人俱樂部的入場限制與 429 標誌的霓虹招牌](/media/editor_temp/6/c96bb1d6-7cde-48a0-98aa-07396570391d.png) * **nginx**:在大量流量/攻擊面前,第一道防線,快速截斷 * **DRF throttling**:在應用層面,根據端點意義與成本,精細化限制 特別是「視圖別差異化」的限制,DRF throttling 既易於實作,又能在伺服器環境變更時保持程式碼一致,極具彈性。