# rdms-spring-boot-starter-protection ## 模块定位 `rdms-spring-boot-starter-protection` 用于提供一组服务保护能力,减少业务代码里重复编写“防重复提交、限流、分布式锁、接口签名校验”这类横切逻辑。 当前模块实际包含四块能力: 1. 幂等:防止同一请求在短时间内被重复执行,例如重复提交、重复点击 2. 分布式锁:在并发场景下保证同一业务动作同一时间只有一个线程或节点执行 3. 限流:限制单位时间内的访问次数,避免接口被刷爆或高频调用 4. HTTP API 签名:校验调用方身份、请求时效和随机数,防止伪造请求和重放攻击 它的目标不是提供复杂的治理平台,而是把常见保护能力做成可声明、可复用、可分布式生效的基础设施。 ## 设计思路 ## 1. 用注解声明保护规则,用 AOP 统一执行 模块里的幂等、限流、签名校验都不是要求业务手写 Redis 判断逻辑,而是通过注解声明规则,再由 AOP 统一拦截执行: 1. `@Idempotent` 2. `@RateLimiter` 3. `@ApiSignature` 这样业务代码只负责说明“这个方法需要什么保护”,具体的 Redis Key 生成、重复请求判断、限流判断、签名验证由框架层统一处理。 ## 2. 把状态统一放到 Redis,面向分布式部署 这个模块的保护能力不是单机内存级别,而是默认面向多实例部署场景: 1. 幂等使用 `StringRedisTemplate` 2. API 签名防重放使用 `StringRedisTemplate` 3. 限流使用 `Redisson` 的 `RRateLimiter` 4. 分布式锁接入 `lock4j-redisson-spring-boot-starter` 这意味着它的核心价值不是“本机防抖”,而是“多个节点共享保护状态”。 ## 3. 单一能力单独建模,避免职责混杂 从实现上看,模块有一个比较明确的边界划分: 1. 幂等用于“同一请求短时间内只允许执行一次” 2. 分布式锁用于“并发竞争下只允许一个线程/节点进入” 3. 限流用于“单位时间窗口内限制请求次数” 4. API 签名用于“校验调用方身份、请求时效和防重放” 例如幂等组件没有扩展成“成功后立即删 Key”的锁语义,而是把这类能力交给 Lock4j 处理。 区分这两类能力时,可以抓住一个核心点: 1. 幂等控制的是“短时间内不要重复执行同一类请求” 2. 分布式锁控制的是“同一时间谁有资格进入临界区执行” 例如: 1. 用户连续点击两次“提交订单”,更适合用幂等 2. 两个线程同时刷新同一份缓存,只允许一个线程进入执行,更适合用分布式锁 一个常见误区是把长耗时任务也交给幂等处理。假设幂等窗口配置为 3 秒,但某个任务实际执行了 20 秒: 1. 第一个请求在第 0 秒进入 2. 幂等 Key 在第 3 秒过期 3. 第二个请求在第 5 秒再次进入 4. 此时第一个任务还没执行完,但第二个请求已经有机会再次执行 所以幂等更偏“防重复请求”,而不是“在整个执行期间控制线程执行权”。如果真正关心的是执行过程中的互斥,应优先使用分布式锁。 ## 4. 用 KeyResolver 抽象不同粒度的保护范围 幂等和限流都没有把 Key 生成规则写死,而是抽成了 `KeyResolver`: 1. 默认级别:方法名 + 参数 2. 用户级别:方法名 + 参数 + 当前用户 3. IP 级别:方法名 + 参数 + 客户端 IP 4. 节点级别:方法名 + 参数 + 当前服务节点 5. 表达式级别:通过 SpEL 自定义 Key 这样模块能在“全局、按用户、按 IP、按节点、按业务字段”之间切换,而不是只能支持一种固定粒度。 ## 5. 自动装配按功能块拆分 自动配置入口见: `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 当前注册了四个自动配置类: 1. `RdmsIdempotentConfiguration` 2. `RdmsLock4jConfiguration` 3. `RdmsRateLimiterConfiguration` 4. `RdmsApiSignatureAutoConfiguration` 这说明模块是按能力块拆分装配的,而不是堆在一个总配置类里。 ## 功能说明 ## 1. 幂等 核心类: 1. `Idempotent` 2. `IdempotentAspect` 3. `IdempotentRedisDAO` 实现方式: 1. AOP 拦截带 `@Idempotent` 的方法 2. 通过 `IdempotentKeyResolver` 解析 Redis Key 3. 调用 `SETNX + EXPIRE` 语义的 `setIfAbsent` 4. 锁定失败时抛出“重复请求”异常 5. 业务异常时可按配置删除 Key,允许后续重试 适合场景: 1. 防止用户双击按钮 2. 防止表单重复提交 3. 防止短时间内重复触发同一个业务动作 典型案例: 1. 创建工单时,前端连续点击两次“提交”,只允许创建一次 2. 导入任务提交后,用户刷新页面再次点击“开始导入”,短时间内不允许重复发起 3. 审批流提交节点时,浏览器因为网络抖动自动重发请求,后端只执行一次 边界说明: 1. 这里的幂等更偏“短时间窗口防重复请求”,不适合作为支付、退款这类长期业务幂等的唯一方案 2. 对于支付回调、订单状态推进这类场景,更可靠的做法仍然是基于业务唯一号和持久化状态做幂等判断,必要时再配合分布式锁 可选 KeyResolver: 1. `DefaultIdempotentKeyResolver` 2. `UserIdempotentKeyResolver` 3. `ExpressionIdempotentKeyResolver` 示例: ```java @Idempotent(timeout = 3, message = "请勿重复提交") public Long createOrder(OrderCreateReqVO reqVO) { return orderService.createOrder(reqVO); } ``` 如果需要按某个参数做幂等,可以使用表达式: ```java @Idempotent( keyResolver = ExpressionIdempotentKeyResolver.class, keyArg = "#reqVO.no", timeout = 10 ) public void submit(OrderReqVO reqVO) { } ``` 如果接口已经完成登录鉴权,也可以按“当前登录用户 + 接口参数”做幂等: ```java @Idempotent( keyResolver = UserIdempotentKeyResolver.class, timeout = 3, message = "请勿重复提交" ) public CommonResult createOrder(OrderCreateReqVO reqVO) { return success(orderService.createOrder(reqVO)); } ``` 这种方式不要求把 `userId` 显式放在请求参数里,而是从当前登录上下文中获取用户信息,适合“同一个用户短时间内不能重复提交同一类请求”的场景。 ## 2. 限流 核心类: 1. `RateLimiter` 2. `RateLimiterAspect` 3. `RateLimiterRedisDAO` 实现方式: 1. AOP 在方法执行前拦截 2. 通过 `RateLimiterKeyResolver` 解析限流 Key 3. 使用 Redisson 的 `RRateLimiter` 设置速率 4. 超过限制时抛出“请求过于频繁”异常 适合场景: 1. 接口防刷 2. 高频查询保护 3. 短时间内限制短信、验证码、导出等高成本操作 4. 对开放接口或公网接口做基础流量保护 典型案例: 1. 短信验证码接口限制“1 分钟内最多发送 1 次” 2. 导出 Excel 接口限制“10 分钟内最多导出 3 次” 3. 登录接口按 IP 限制访问频率,防止暴力尝试 4. 某个开放查询接口按调用方或用户维度限制 QPS,避免被刷爆 可选 KeyResolver: 1. `DefaultRateLimiterKeyResolver` 2. `UserRateLimiterKeyResolver` 3. `ClientIpRateLimiterKeyResolver` 4. `ServerNodeRateLimiterKeyResolver` 5. `ExpressionRateLimiterKeyResolver` 示例: ```java @RateLimiter(count = 5, time = 1, timeUnit = TimeUnit.MINUTES, message = "请求过于频繁") public CommonResult sendSmsCode(String mobile) { return success(true); } ``` 如果希望按用户限流: ```java @RateLimiter( count = 10, time = 1, timeUnit = TimeUnit.MINUTES, keyResolver = UserRateLimiterKeyResolver.class ) public PageResult pageMyOrders(OrderPageReqVO reqVO) { return orderService.pageMyOrders(reqVO); } ``` ## 3. HTTP API 签名 核心类: 1. `ApiSignature` 2. `ApiSignatureAspect` 3. `ApiSignatureRedisDAO` 实现方式: 1. 从请求头中读取 `appId`、`timestamp`、`nonce`、`sign` 2. 校验时间戳是否过期 3. 校验 `nonce` 是否已使用,防止重放 4. 根据 `appId` 从 Redis 中获取 `appSecret` 5. 按“请求参数 + 请求体 + 请求头 + 密钥”构建签名字符串 6. 使用 `SHA-256` 计算服务端签名并比对 适合场景: 1. 开放接口 2. 系统间调用 3. 对请求来源和重放攻击有要求的 HTTP 接口 典型案例: 1. 第三方系统调用内部开放 API 时,使用 `appId + appSecret` 做签名校验 2. 支付平台、供应商平台、合作方系统调用回调接口时,先校验签名再进入业务处理 3. 对外提供的 B2B 接口要求请求在 60 秒内有效,并且 `nonce` 只能使用一次 示例: ```java @ApiSignature(timeout = 60) public CommonResult callback() { return success("ok"); } ``` 当前切面是按 `@annotation(signature)` 拦截的,因此实操上应优先加在方法上。 ## 4. 分布式锁 核心类: 1. `RdmsLock4jConfiguration` 2. `DefaultLockFailureStrategy` 这部分没有重复实现分布式锁,而是接入 Lock4j,并补了一层默认失败策略: 1. 如果类路径存在 `Lock4j`,则启用配置 2. 在获取锁失败时,统一抛出项目内的 `ServiceException` 3. 保持锁失败时的错误处理风格一致 适合场景: 1. 防止同一业务动作并发执行 2. 串行化关键资源操作 3. 抢占式处理任务 典型案例: 1. 同一个用户同时触发两次“刷新缓存”,只允许一个线程进入 2. 定时任务集群部署时,同一时间只允许一个节点执行清理任务 3. 库存扣减、状态迁移、批量结算这类关键动作,在并发场景下需要串行处理 示例: ```java @Lock4j(keys = "#userId", expire = 30000, acquireTimeout = 1000) public void refreshUserCache(Long userId) { } ``` ## 自动装配链路 ## 1. Spring Boot 自动配置入口 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 1. `com.njcn.rdms.framework.idempotent.config.RdmsIdempotentConfiguration` 2. `com.njcn.rdms.framework.lock4j.config.RdmsLock4jConfiguration` 3. `com.njcn.rdms.framework.ratelimiter.config.RdmsRateLimiterConfiguration` 4. `com.njcn.rdms.framework.signature.config.RdmsApiSignatureAutoConfiguration` ## 2. 各配置类负责的事情 | 配置类 | 作用 | | --- | --- | | `RdmsIdempotentConfiguration` | 注册幂等切面、幂等 Redis DAO、幂等 KeyResolver | | `RdmsLock4jConfiguration` | 在 Lock4j 存在时注册默认加锁失败策略 | | `RdmsRateLimiterConfiguration` | 注册限流切面、限流 Redis DAO、限流 KeyResolver | | `RdmsApiSignatureAutoConfiguration` | 注册 API 签名切面、签名 Redis DAO | ## 如何使用 ## 1. 引入依赖 业务模块通常直接依赖: ```xml com.njcn rdms-spring-boot-starter-protection ``` ## 2. 基础前提 这个模块依赖 Redis,因此至少需要可用的 Redis 配置。 其中: 1. 幂等依赖 `StringRedisTemplate` 2. API 签名依赖 `StringRedisTemplate` 3. 限流依赖 `RedissonClient` 4. 分布式锁依赖 Lock4j + Redisson ## 3. 选择合适的保护手段 1. 防重复提交,优先使用 `@Idempotent` 2. 控制访问频率,优先使用 `@RateLimiter` 3. 控制并发进入,优先使用 Lock4j 4. 保护开放接口,优先使用 `@ApiSignature` 不要把它们混成一类能力: 1. 幂等不等于分布式锁 2. 限流不等于幂等 3. API 签名不等于权限校验 ## 4. KeyResolver 的选择建议 1. 默认对全局请求做保护,使用默认 Resolver 2. 需要按登录用户隔离保护,使用 User Resolver 3. 需要按 IP 控制访问,使用 ClientIp Resolver 4. 需要按业务字段控制粒度,使用 Expression Resolver ## 注意事项 1. 幂等的 `timeout` 只是保护窗口,不是业务执行超时时间。如果方法执行时间长于这个窗口,后续请求仍可能进入。 2. 幂等在异常时默认删除 Key,这是为了允许业务失败后重新提交;如果追求的是“执行期间只允许一个线程进入”,更适合使用分布式锁。 3. 限流底层使用 Redisson 的 `RRateLimiter`,运行环境需要有 `RedissonClient`。 4. API 签名依赖 Redis 中预先存在 `appId -> appSecret` 的映射,否则签名校验无法通过。 5. API 签名当前实操上应加在方法上,避免把类级注解误认为一定会生效。 6. 这个模块的保护能力都是方法级横切逻辑,适合放在 Controller 或 Service 的明确边界上使用,不适合到处滥加。 ## 总结 这个模块的核心价值,是把服务保护相关的高频横切问题统一沉到框架层: 1. 如何防止重复提交 2. 如何限制单位时间的访问频率 3. 如何在分布式环境下串行化关键操作 4. 如何校验开放接口的请求签名和防重放 如果把它理解成一个“请求保护与并发保护能力集合”的 starter,这个模块的设计会比较清晰。