这个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 响应
{
"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 = "/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 = "/plan", produces = MediaType.APPLICATION\_JSON\_VALUE)
**public** ResponseEntity<List<Plan>> retrieve() {
**var** response = planService.retrieve();
**return** ResponseEntity.ok(response);
}
[https://www.jdon.com/72862.html](https://www.jdon.com/72862.html