SpringBoot中實現API速率限制的令牌桶演算法專案

banq發表於2024-03-09


這個github專案是利用Bucket4j以及 Redis 快取和 Spring Security 過濾器對私有 API 端點實施速率限制。

需要升級到 Spring Boot 3 和 Spring Security 6

關鍵元件:

  • RedisConfiguration.java
  • RateLimitingService.java
  • RateLimitFilter.java
  • BypassRateLimit.java
  • PublicEndpoint.java
  • Flyway Migration Scripts

流程:
在應用程式的初始啟動過程中,將使用 Flyway 遷移指令碼建立資料庫表並填充資料。具體來說,計劃表中會填充預定義的計劃,每個計劃都會分配一個特定的 limit_per_hour 值。

  • FREE    20
  • BUSINESS    40
  • PROFESSIONAL    100

建立使用者賬戶時,作為請求的一部分傳送的指定計劃 ID 將與使用者記錄關聯。該計劃決定了適用於該使用者的費率限制配置。

當使用者使用認證成功後收到的有效 JWT 令牌呼叫私有 API 端點時,應用程式會根據使用者選擇的計劃執行速率限制。速率限制在 RateLimitFilter 中執行,當前配置由 RateLimitingService 管理。

首次呼叫 API 時,RateLimitingService 會從資料來源獲取使用者的計劃詳情,並將其儲存在快取中,以便在後續請求中有效檢索。這些以 Bucket 形式儲存的資料用於使用 Bucket4j 實現令牌 Bucket 演算法。

當某個使用者分配的速率限制耗盡時,將向客戶端傳送以下 API 響應

{
  <font>"Status": "429 TOO_MANY_REQUESTS",
 
"Description": "API request limit linked to your current plan has been exhausted."
}

可以更新當前使用者計劃,從而刪除快取中儲存的以前的速率限制配置。更新計劃的私有 API 端點已配置為使用 @BypassRateLimit 繞過費率限制檢查,即使當前費率限制已用盡,也允許透過有效的 JWT 令牌進行訪問。

速率限制標頭
根據使用者的速率限制評估傳入 HTTP 請求後,RateLimitFilter 會在響應中包含額外的 HTTP 標頭,以提供更多資訊。這些標頭有助於客戶端應用程式瞭解速率限制狀態,並相應調整其行為,以從容應對違反速率限制的情況。

繞過速率限制執行
透過使用註釋來註釋相應的控制器方法,可以繞過特定私有 API 端點的速率限制強制執行@BypassRateLimit。應用後,對該方法的請求不會受到RateLimitFilter.java的速率限制,並且無論使用者當前的速率限制計劃如何,都將被允許。

下面用於更新使用者當前計劃的私有 API 端點帶有註釋,@BypassRateLimit以確保更新到新計劃的請求不受使用者速率限制的限制。

@BypassRateLimit
@PutMapping(value = <font>"/api/v1/plan")
public ResponseEntity<HttpStatus> update(@RequestBody PlanUpdationRequest planUpdationRequest) {
    planService.update(planUpdationRequest);
    return ResponseEntity.status(HttpStatus.OK).build();
}


安全過濾器
所有對私有 API 端點的請求都會被JwtAuthenticationFilter攔截。該過濾器負責驗證傳入訪問令牌的簽名並填充安全上下文。僅當訪問令牌的簽名成功驗證後,請求才會到達RateLimitFilter,後者相應地對使用者實施速率限制。

這兩個自定義過濾器都新增到 Spring Security 過濾器鏈中並在 SecurityConfiguration 中進行配置。

任何需要公開的 API 都可以使用@PublicEndpoint進行註釋。對配置的 API 路徑的請求不會由任何一個過濾器進行評估,其邏輯由ApiEndpointSecurityInspector控制。

以下是宣告為公共的示例控制器方法,該方法將免除身份驗證檢查:

@PublicEndpoint
@GetMapping(value = <font>"/plan", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Plan>> retrieve() {
    var response = planService.retrieve();
    return ResponseEntity.ok(response);
}

 

相關文章