Files
cn-rdms/rdms-framework/rdms-spring-boot-starter-protection/README.md
2026-03-11 19:32:37 +08:00

374 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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这个模块的设计会比较清晰。