跳到内容

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计数执行只负责单次消费额度和返回窗口状态,不抛业务异常
@RateLimitedWebMVC 声明式入口把 Controller 类 / 方法注解转换为 RateLimitRequest

核心流程:

  1. 业务传入 key + maxAttempts + window + cost
  2. DefaultRateLimitManager 合并默认配置,并给 key 补齐统一前缀。
  3. 按请求 store 或 default-store 选择 memory / local-token-bucket / redis / redisson。
  4. Store 执行消费并返回 allowed / remaining / resetAt / retryAfter
  5. assertAllowed 在拒绝时统一抛出 TOO_MANY_REQUESTS

Store 语义:

Store机制适用
memoryJVM 内存固定窗口,定期清理过期窗口单测、隔离诊断、lite 兼容模式
local-token-bucketJVM 内存令牌桶,按 maxAttempts / window 平滑补充令牌,允许容量内突发单实例热点防刷、临时削峰、本地诊断
redisRedis 原子自增固定窗口,首次命中设置 TTL多实例生产、登录、验证码、普通接口
redissonRedisson 分布式配额桶,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。

声明式限流会在响应中写入通用限流头:

http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1779408060
Retry-After: 12

Retry-After 只在被限流时返回。普通接口被限流时仍抛统一业务码 429silent=true 仅适合心跳、日志上报、埋点等允许空响应的接口,被限流时返回 204 No Content 并停止进入 Controller。

快速开始

yaml
spring:
  open:
    rate-limit:
      enabled: true
      default-store: redis           # 生产推荐
java
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));

声明式:

java
@RateLimited(maxAttempts = 10, windowSeconds = 60)
@GetMapping("/api/v1/admin/reporting/reports/export")
public ResponseEntity<?> export() { ... }

Facade API

java
// 编程式
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 默认窗口
cost1当前请求消耗额度
store可选 store 名;为空用默认 store
keyStrategyIP_ROUTEGLOBAL / IP / USER / ROUTE / IP_ROUTE / USER_ROUTE
silentfalse被限流时是否返回 204 No Content 并静默丢弃;只用于心跳、埋点、日志上报等可丢请求

声明式限流只在 WebMVC 环境自动生效。默认 IP_ROUTE 适合普通接口防刷;客户端 IP 优先从 X-Forwarded-ForX-Real-IPCF-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 身份,适合保护匿名入口,但不适合区分已登录用户额度:

java
@Bean
RateLimitIdentityResolver rateLimitIdentityResolver() {
    return request -> currentUserIdOrAppKey();
}

@RateLimited(maxAttempts = 20, windowSeconds = 60, keyStrategy = RateLimitKeyStrategy.USER_ROUTE)
@PostMapping("/api/v1/blog/articles")
public ResponseEntity<?> createArticle() { ... }

也可放在 Controller 类上对所有接口生效:

java
@RateLimited(key = "report-admin", maxAttempts = 120, windowSeconds = 60)
@RestController
class ReportApiController { ... }

观测指标

应用引入 Micrometer MeterRegistry 后,Rate Limit starter 会自动记录限流消费计数:

指标类型标签说明
spring.open.rate.limit.requestsCounterstoreoutcome限流消费请求数量

outcome 取值:

说明
allowed已消耗额度且允许通过
rejected已触发限流拒绝
unlimited本次请求配置为不限流

指标不会把业务 key、用户名、IP、手机号、应用 key 等高基数字段写入标签;需要按租户、应用或 scope 做更细报表时,应在业务模块基于自身审计表或报表表聚合。

后台运维

spring-open-core-rate-limit 提供只读后台运维入口:

说明
APIGET /api/v1/admin/rate-limit/operations
权限rate-limit:view
菜单任务运维 / 限流运维
前端页面/#/rate-limit/operations

运维快照展示默认 store、默认窗口、默认次数、声明式限流开关、可信代理配置、已注册 store、限流响应头、Micrometer 指标可用性和生产 runbook 提示。该页面不修改运行时配置,不替代配置中心;需要调整默认 store、可信代理或窗口额度时仍通过 spring.open.rate-limit.* 配置或配置中心的 owning key 完成。

配置项

yaml
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
配置默认值说明
enabledtrue启用开关
default-storememory(应用装配 redis,lite memory默认 store
default-window1m默认固定窗口
default-max-attempts60默认最大次数(非正数 = 不限流)
key-prefixrate-limit限流 key 前缀
annotation.enabledtrue@RateLimited 注解
annotation.key-prefixweb声明式接口限流 key 前缀
stores.memory.cleanup-interval1024内存 store 清理过期窗口的调用间隔
stores.local-token-bucket.enabledtrue本地令牌桶 store
stores.local-token-bucket.cleanup-interval1024本地令牌桶清理闲置桶的调用间隔
stores.redis.enabledtrueRedis store
stores.redisson.enabledtrueRedisson 配额桶 store

Provider / Store

Store说明适用
memoryJVM 内存固定窗口单测、隔离诊断、lite 兼容模式
local-token-bucketJVM 内存令牌桶,允许桶容量内突发并按窗口平滑补充单实例临时防刷、热点接口平滑削峰
redisRedis 原子递增固定窗口多实例生产
redissonRedisson RRateLimiter 配额桶AI 网关、开放平台、高成本接口

Cache Facade 作为限流底层。限流需要原子递增、窗口过期或配额桶语义,Redis store 直接复用 RedisManager,Redisson store 复用 RedissonManager,避免把 Cache 做成 Redis 的复刻版。

redisredisson 的差异:

  • redis 是固定窗口,适合登录、验证码、普通接口和开放平台每分钟调用次数。
  • local-token-bucket 是本地令牌桶,maxAttempts 表示桶容量,windowSeconds 表示补满窗口;例如 60 / 60s 约等于平均 1 QPS,并允许短时消耗最多 60 个令牌。
  • redisson 是分布式配额桶,适合 AI 网关、导出、批处理等高成本接口。
  • redissonresetAt 是基于配置窗口计算的重试提示,不等同于固定窗口清零时间。
  • 二者都使用项目 Redis key 前缀,不需要业务模块直接接触 Redis / Redisson API。

常见场景

登录限流

java
RateLimit.assertAllowed("login:username:" + username, 5, Duration.ofMinutes(1));
RateLimit.assertAllowed("login:ip:" + requestIp, 20, Duration.ofMinutes(1));

IAM 账号密码登录已接入:

yaml
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}

短信 / 邮箱验证码限流

java
RateLimit.assertAllowed("sms:mobile:" + mobile, 1, Duration.ofMinutes(1));
RateLimit.assertAllowed("sms:ip:" + requestIp, 30, Duration.ofHours(1));

IAM 验证码已接入:

http
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/check
yaml
spring:
  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 通道。

普通接口限流

java
RateLimitResult result = rateLimitManager.consume(
    RateLimitRequest.of("api:report:export:user:" + userId, 10, Duration.ofMinutes(1)));

或声明式:

java
@RateLimited(maxAttempts = 10, windowSeconds = 60)
@GetMapping("/api/v1/admin/reporting/reports/export")
public ResponseEntity<?> export() { ... }

单实例临时削峰可显式切换本地令牌桶:

java
@RateLimited(maxAttempts = 60, windowSeconds = 60, store = "local-token-bucket")
@GetMapping("/api/v1/cms/contents")
public ResponseEntity<?> listContents() { ... }

心跳、埋点、日志上报等允许丢弃的接口可以使用静默限流:

java
@RateLimited(maxAttempts = 30, windowSeconds = 60, silent = true)
@PostMapping("/api/v1/client/events")
public ResponseEntity<?> collectEvents() { ... }

Open Platform 限流

已接入通用 Rate Limit。应用级业务配置:

yaml
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 响应头:

http
X-Open-RateLimit-Limit: 60
X-Open-RateLimit-Remaining: 59
X-Open-RateLimit-Reset: 1779408060
Retry-After: 12

Retry-After 只在被限流时返回。

设计建议

  • 登录 + 短信 同时按账号、手机号、IP 做组合限流
  • 对外开放 APIappKey 限流;付费套餐时可绑定额度到应用或 scope
  • 管理后台导出 / 批量操作 单独设置更低额度
  • 生产多实例必须用 redis store
  • 单实例热点接口需要平滑削峰时可使用 local-token-bucket;不要把它当作多实例生产的统一额度来源
  • 网关限流作为外层防护,但业务语义限流保留在应用层(按用户 / 应用 / scope / 手机号等业务维度)

注意事项

  • 限流 key 必须包含业务维度(账号 / IP / appKey / userId),不要全局共享 key
  • 敏感标识(账号 / IP / 手机号)哈希后写入 key,明文
  • 失败 / 异常时不应消费额度(业务逻辑判断后再 consume)
  • memory / redis 是固定窗口,临界点可能有突刺;单实例可用 local-token-bucket 平滑削峰,多实例高成本接口优先用 redisson
  • USER / USER_ROUTE 没有业务身份解析器时会统一落到匿名身份,登录后接口必须提供 RateLimitIdentityResolver
  • silent=true 不适合登录、下单、支付、AI 网关等必须让调用方感知失败的接口

验证命令

bash
./mvnw -pl spring-open-starters/spring-open-starter-rate-limit -am test -DskipITs
./mvnw -pl spring-open-application -am test -DskipITs -DskipFrontend

相关

Released under the Apache License 2.0.