374 lines
13 KiB
Markdown
374 lines
13 KiB
Markdown
|
|
# 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,这个模块的设计会比较清晰。
|