Files
cn-rdms/rdms-framework/rdms-spring-boot-starter-protection/README.md

374 lines
13 KiB
Markdown
Raw Normal View History

2026-03-11 19:32:37 +08:00
# 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<Long> 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<Boolean> sendSmsCode(String mobile) {
return success(true);
}
```
如果希望按用户限流:
```java
@RateLimiter(
count = 10,
time = 1,
timeUnit = TimeUnit.MINUTES,
keyResolver = UserRateLimiterKeyResolver.class
)
public PageResult<OrderRespVO> 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<String> 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
<dependency>
<groupId>com.njcn</groupId>
<artifactId>rdms-spring-boot-starter-protection</artifactId>
</dependency>
```
## 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这个模块的设计会比较清晰。