Rate Limit Starter
受众:开发者 摘要:通用限流底座。memory / local-token-bucket / redis / redisson store,登录 / 短信验证码 / 开放平台 / 普通接口 / 声明式
@RateLimited全覆盖。
何时使用
| 场景 | 建议 |
|---|---|
| 登录密码限流(按账号 + IP) | RateLimit.assertAllowed("login:username:...", 5, 1m) |
| 短信 / 邮箱验证码限流 | RateLimit.assertAllowed("sms:mobile:...", 1, 1m) |
| 普通接口防刷 | @RateLimited(maxAttempts = 60, windowSeconds = 60) |
| 开放平台按 appKey 限流 | spring.open.open-platform.security.default-rate-limit-per-minute |
| 高成本接口(导出 / 批量操作) | 单独设置低额度声明式限流 |
| 多实例生产 | 常规固定窗口用 redis,高成本配额桶用 redisson |
| 单实例热点防刷 / 平滑削峰 | 可选 local-token-bucket store |
| 单元测试 / 单进程诊断 / lite 兼容模式 | memory store |
| 网关层粗粒度限流 | 用 Nginx / API Gateway,本 starter 是业务层语义限流 |
业务代码只用 RateLimit Facade / RateLimitManager,不直接绑定 Redis / 内存计数器 / 网关实现。
机制说明
Rate Limit starter 是业务语义限流,不是网关限流的替代品。它把“登录失败次数、短信发送频率、开放平台调用额度、导出接口消耗”等业务维度统一抽象为一次 consume。
| 层级 | 职责 | 说明 |
|---|---|---|
RateLimit | 静态 Facade | 业务代码最常用入口,要求 starter 已注入 RateLimitManager |
RateLimitManager | 策略合并 | 补齐默认窗口、默认次数、统一 key 前缀和 store 选择 |
RateLimitStore | 计数执行 | 只负责单次消费额度和返回窗口状态,不抛业务异常 |
@RateLimited | WebMVC 声明式入口 | 把 Controller 类 / 方法注解转换为 RateLimitRequest |
核心流程:
- 业务传入
key + maxAttempts + window + cost。 DefaultRateLimitManager合并默认配置,并给 key 补齐统一前缀。- 按请求 store 或
default-store选择 memory / local-token-bucket / redis / redisson。 - Store 执行消费并返回
allowed / remaining / resetAt / retryAfter。 assertAllowed在拒绝时统一抛出TOO_MANY_REQUESTS。
Store 语义:
| Store | 机制 | 适用 |
|---|---|---|
memory | JVM 内存固定窗口,定期清理过期窗口 | 单测、隔离诊断、lite 兼容模式 |
local-token-bucket | JVM 内存令牌桶,按 maxAttempts / window 平滑补充令牌,允许容量内突发 | 单实例热点防刷、临时削峰、本地诊断 |
redis | Redis 原子自增固定窗口,首次命中设置 TTL | 多实例生产、登录、验证码、普通接口 |
redisson | Redisson 分布式配额桶,key 带限额和窗口 | 开放平台、AI 网关、导出等高成本接口 |
声明式限流的默认 key 策略是 IP_ROUTE。客户端 IP 做 SHA-256 哈希后写入限流 key,不记录明文 IP。代理头(X-Forwarded-For / X-Real-IP / CF-Connecting-IP 等)只有在连接对端命中可信代理时才解析,X-Forwarded-For 按从右到左取第一个非可信代理地址,防止伪造头绕过 IP 限流;可信代理默认为回环、私网、链路本地和 IPv6 ULA 地址段,可用 spring.open.rate-limit.annotation.client-ip.trusted-proxies(IP / CIDR 列表)显式收窄,trust-forwarded-headers: false 则完全忽略代理头(适合无反向代理直连部署)。类注解可作为默认规则,方法注解优先生效。
@RateLimited 允许声明 USER / USER_ROUTE 策略。Rate Limit starter 不依赖 IAM 或 Open Platform,业务方可注册 RateLimitIdentityResolver Bean 返回用户 ID、租户 ID、应用 key 等稳定身份;未注册或返回空白时使用匿名身份。身份同样会先做 SHA-256 后进入限流 key。
声明式限流会在响应中写入通用限流头:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1779408060
Retry-After: 12Retry-After 只在被限流时返回。普通接口被限流时仍抛统一业务码 429;silent=true 仅适合心跳、日志上报、埋点等允许空响应的接口,被限流时返回 204 No Content 并停止进入 Controller。
快速开始
spring:
open:
rate-limit:
enabled: true
default-store: redis # 生产推荐RateLimitResult result = RateLimit.consume("login:ip:127.0.0.1", 5, Duration.ofMinutes(1));
if (!result.allowed()) {
throw new FrameworkException(GlobalResultCode.TOO_MANY_REQUESTS);
}
// 或直接断言
RateLimit.assertAllowed("sms:mobile:13800000000", 1, Duration.ofMinutes(1));声明式:
@RateLimited(maxAttempts = 10, windowSeconds = 60)
@GetMapping("/api/v1/admin/reporting/reports/export")
public ResponseEntity<?> export() { ... }Facade API
// 编程式
RateLimitResult result = RateLimit.consume(key, maxAttempts, window);
RateLimit.assertAllowed(key, maxAttempts, window); // 直接断言,被限流时抛 FrameworkException
// 详细查询
RateLimitResult result = rateLimitManager.consume(
RateLimitRequest.of(key, maxAttempts, window));RateLimitResult 字段
| 字段 | 说明 |
|---|---|
allowed | 当前请求是否允许 |
key | 规范化后的限流 key |
limit | 当前窗口最大次数,0 = 不限流 |
remaining | 当前窗口剩余次数 |
resetAt | 窗口重置时间 |
retryAfter | 被拒绝时建议等待时间 |
@RateLimited 注解参数
| 参数 | 默认值 | 说明 |
|---|---|---|
key | 空 | 业务 key;为空时使用 HTTP method + route pattern |
maxAttempts | -1 | 窗口最大次数;≤ 0 时用 starter 默认值 |
windowSeconds | -1 | 固定窗口秒数;≤ 0 时用 starter 默认窗口 |
cost | 1 | 当前请求消耗额度 |
store | 空 | 可选 store 名;为空用默认 store |
keyStrategy | IP_ROUTE | GLOBAL / IP / USER / ROUTE / IP_ROUTE / USER_ROUTE |
silent | false | 被限流时是否返回 204 No Content 并静默丢弃;只用于心跳、埋点、日志上报等可丢请求 |
声明式限流只在 WebMVC 环境自动生效。默认 IP_ROUTE 适合普通接口防刷;客户端 IP 优先从 X-Forwarded-For、X-Real-IP、CF-Connecting-IP 等常见代理头解析,哈希后写入 key,不记录明文 IP。
普通拒绝会先写入 X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset / Retry-After,再抛出统一 TOO_MANY_REQUESTS。在项目 WebMVC 统一响应约定下,最终 HTTP status 仍为 200,业务响应体 Result.code=429;外部协议网关如需原生 HTTP 429,应在网关层单独适配。
按当前调用身份限流时,业务模块提供解析器即可,不需要把 IAM / Open Platform 依赖反向塞进 starter。解析器返回空或未注册解析器时会退化为 anonymous 身份,适合保护匿名入口,但不适合区分已登录用户额度:
@Bean
RateLimitIdentityResolver rateLimitIdentityResolver() {
return request -> currentUserIdOrAppKey();
}
@RateLimited(maxAttempts = 20, windowSeconds = 60, keyStrategy = RateLimitKeyStrategy.USER_ROUTE)
@PostMapping("/api/v1/blog/articles")
public ResponseEntity<?> createArticle() { ... }也可放在 Controller 类上对所有接口生效:
@RateLimited(key = "report-admin", maxAttempts = 120, windowSeconds = 60)
@RestController
class ReportApiController { ... }观测指标
应用引入 Micrometer MeterRegistry 后,Rate Limit starter 会自动记录限流消费计数:
| 指标 | 类型 | 标签 | 说明 |
|---|---|---|---|
spring.open.rate.limit.requests | Counter | store、outcome | 限流消费请求数量 |
outcome 取值:
| 值 | 说明 |
|---|---|
allowed | 已消耗额度且允许通过 |
rejected | 已触发限流拒绝 |
unlimited | 本次请求配置为不限流 |
指标不会把业务 key、用户名、IP、手机号、应用 key 等高基数字段写入标签;需要按租户、应用或 scope 做更细报表时,应在业务模块基于自身审计表或报表表聚合。
后台运维
spring-open-core-rate-limit 提供只读后台运维入口:
| 项 | 说明 |
|---|---|
| API | GET /api/v1/admin/rate-limit/operations |
| 权限 | rate-limit:view |
| 菜单 | 任务运维 / 限流运维 |
| 前端页面 | /#/rate-limit/operations |
运维快照展示默认 store、默认窗口、默认次数、声明式限流开关、可信代理配置、已注册 store、限流响应头、Micrometer 指标可用性和生产 runbook 提示。该页面不修改运行时配置,不替代配置中心;需要调整默认 store、可信代理或窗口额度时仍通过 spring.open.rate-limit.* 配置或配置中心的 owning key 完成。
配置项
spring:
open:
rate-limit:
enabled: true
annotation:
enabled: true
key-prefix: web
default-store: redis # memory 单机 / redis 固定窗口 / redisson 配额桶
default-window: 1m
default-max-attempts: 60
key-prefix: rate-limit
stores:
memory:
provider: memory
cleanup-interval: 1024
local-token-bucket:
provider: local-token-bucket
cleanup-interval: 1024
redis:
provider: redis
enabled: true
redisson:
provider: redisson
enabled: true| 配置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 启用开关 |
default-store | memory(应用装配 redis,lite memory) | 默认 store |
default-window | 1m | 默认固定窗口 |
default-max-attempts | 60 | 默认最大次数(非正数 = 不限流) |
key-prefix | rate-limit | 限流 key 前缀 |
annotation.enabled | true | @RateLimited 注解 |
annotation.key-prefix | web | 声明式接口限流 key 前缀 |
stores.memory.cleanup-interval | 1024 | 内存 store 清理过期窗口的调用间隔 |
stores.local-token-bucket.enabled | true | 本地令牌桶 store |
stores.local-token-bucket.cleanup-interval | 1024 | 本地令牌桶清理闲置桶的调用间隔 |
stores.redis.enabled | true | Redis store |
stores.redisson.enabled | true | Redisson 配额桶 store |
Provider / Store
| Store | 说明 | 适用 |
|---|---|---|
memory | JVM 内存固定窗口 | 单测、隔离诊断、lite 兼容模式 |
local-token-bucket | JVM 内存令牌桶,允许桶容量内突发并按窗口平滑补充 | 单实例临时防刷、热点接口平滑削峰 |
redis | Redis 原子递增固定窗口 | 多实例生产 |
redisson | Redisson RRateLimiter 配额桶 | AI 网关、开放平台、高成本接口 |
Cache Facade 不作为限流底层。限流需要原子递增、窗口过期或配额桶语义,Redis store 直接复用 RedisManager,Redisson store 复用 RedissonManager,避免把 Cache 做成 Redis 的复刻版。
redis 和 redisson 的差异:
redis是固定窗口,适合登录、验证码、普通接口和开放平台每分钟调用次数。local-token-bucket是本地令牌桶,maxAttempts表示桶容量,windowSeconds表示补满窗口;例如60 / 60s约等于平均 1 QPS,并允许短时消耗最多 60 个令牌。redisson是分布式配额桶,适合 AI 网关、导出、批处理等高成本接口。redisson的resetAt是基于配置窗口计算的重试提示,不等同于固定窗口清零时间。- 二者都使用项目 Redis key 前缀,不需要业务模块直接接触 Redis / Redisson API。
常见场景
登录限流
RateLimit.assertAllowed("login:username:" + username, 5, Duration.ofMinutes(1));
RateLimit.assertAllowed("login:ip:" + requestIp, 20, Duration.ofMinutes(1));IAM 账号密码登录已接入:
spring:
open:
iam:
auth:
login:
rate-limit:
enabled: true
password:
enabled: true
identifier-max-attempts: 5 # 单账号
identifier-window: 1m
ip-max-attempts: 20 # 单 IP
ip-window: 1m
key-prefix: iam:auth:login登录限流 key 对账号和 IP 做哈希处理,不把用户名 / 邮箱 / 手机号 / IP 明文写入 Redis。被限流时返回业务码 429,登录失败审计记录 {messages.exception.too-many-requests}。
短信 / 邮箱验证码限流
RateLimit.assertAllowed("sms:mobile:" + mobile, 1, Duration.ofMinutes(1));
RateLimit.assertAllowed("sms:ip:" + requestIp, 30, Duration.ofHours(1));IAM 验证码已接入:
POST /api/v1/auth/verify/sms-codes # 发送短信验证码
POST /api/v1/auth/verify/sms-codes/check # 校验
POST /api/v1/auth/verify/email-codes
POST /api/v1/auth/verify/email-codes/checkspring:
open:
iam:
auth:
login:
verification:
sms:
enabled: true
default-scene: login
code-length: 6 # 4-10 位
ttl: 5m
mobile-max-attempts: 1 # 单手机号发送
mobile-window: 1m
ip-max-attempts: 30 # 单 IP 发送
ip-window: 1h
verify-mobile-max-attempts: 5 # 单手机号校验
verify-mobile-window: 5m
verify-ip-max-attempts: 60 # 单 IP 校验
verify-ip-window: 1h
key-prefix: iam:auth:login:verification:sms
template: "" # 框架 Template 编码
template-code: "" # 厂商短信模板编码
sign-name: "" # 厂商签名
provider: "" # 指定 SMS provider
email:
enabled: true
# 同结构
email-max-attempts: 1
verify-email-max-attempts: 5
subject: "Verification code"发送和校验都按手机号 / 邮箱 + IP 组合限流。验证码哈希后写 Cache,校验成功立即消费。短信走 Notification sms 通道,邮箱走 mail 通道。
普通接口限流
RateLimitResult result = rateLimitManager.consume(
RateLimitRequest.of("api:report:export:user:" + userId, 10, Duration.ofMinutes(1)));或声明式:
@RateLimited(maxAttempts = 10, windowSeconds = 60)
@GetMapping("/api/v1/admin/reporting/reports/export")
public ResponseEntity<?> export() { ... }单实例临时削峰可显式切换本地令牌桶:
@RateLimited(maxAttempts = 60, windowSeconds = 60, store = "local-token-bucket")
@GetMapping("/api/v1/cms/contents")
public ResponseEntity<?> listContents() { ... }心跳、埋点、日志上报等允许丢弃的接口可以使用静默限流:
@RateLimited(maxAttempts = 30, windowSeconds = 60, silent = true)
@PostMapping("/api/v1/client/events")
public ResponseEntity<?> collectEvents() { ... }Open Platform 限流
已接入通用 Rate Limit。应用级业务配置:
spring:
open:
open-platform:
security:
default-rate-limit-per-minute: 60规则:
- 应用单独配置
rate_limit_per_minute时优先用应用配置 - 应用未配置时用
default-rate-limit-per-minute 0或负数 = 不限流- 计数 / 窗口 / store 切换由 Rate Limit starter 处理;AI 网关等高成本接口可把默认 store 或单次请求 store 切换为
redisson
开放 API 响应头:
X-Open-RateLimit-Limit: 60
X-Open-RateLimit-Remaining: 59
X-Open-RateLimit-Reset: 1779408060
Retry-After: 12Retry-After 只在被限流时返回。
设计建议
- 登录 + 短信 同时按账号、手机号、IP 做组合限流
- 对外开放 API 按
appKey限流;付费套餐时可绑定额度到应用或 scope - 管理后台导出 / 批量操作 单独设置更低额度
- 生产多实例必须用
redisstore - 单实例热点接口需要平滑削峰时可使用
local-token-bucket;不要把它当作多实例生产的统一额度来源 - 网关限流作为外层防护,但业务语义限流保留在应用层(按用户 / 应用 / scope / 手机号等业务维度)
注意事项
- 限流 key 必须包含业务维度(账号 / IP / appKey / userId),不要全局共享 key
- 敏感标识(账号 / IP / 手机号)哈希后写入 key,不明文
- 失败 / 异常时不应消费额度(业务逻辑判断后再 consume)
memory/redis是固定窗口,临界点可能有突刺;单实例可用local-token-bucket平滑削峰,多实例高成本接口优先用redissonUSER/USER_ROUTE没有业务身份解析器时会统一落到匿名身份,登录后接口必须提供RateLimitIdentityResolversilent=true不适合登录、下单、支付、AI 网关等必须让调用方感知失败的接口
验证命令
./mvnw -pl spring-open-starters/spring-open-starter-rate-limit -am test -DskipITs
./mvnw -pl spring-open-application -am test -DskipITs -DskipFrontend相关
- IAM 登录 / 验证码:iam.md
- 开放平台:open-platform.md
- SMS:sms.md
- Notification:notification.md
- Redis 底座:redis.md
- Provider 扩展模型:provider.md