Flow-Limit-Spring-boot-starter,是一个springboot的启动器,提供限流与反爬的解决方案。
- 更新日志:
- v1.0:实现
Redis AOP
计数器限流。 - v1.1:重构启动器结构,使用**
模板方法模式
**。 - v1.2:新增Redis拦截器方式,本质是
Redis AOP
适配,即**适配器模式
**。 - v1.3:
AOP
与Interceptor
可以一起使用,因其执行顺序Interceptor
>AOP
,因此需要准确的配置切点与拦截路径。 - v1.4:配置文件,
prefix
、counterKey
允许为null。修复重大Bug。 - v1.5
- 重构Cache帮助器为:
工厂模式
+策略模式
。 - 策略模式可以更好的拓展系统,目前已经实现
Redis
作为数据源、caffeine
作为本地缓存数据源,mysql尚未实现。 - 考虑到本地缓存是单机模式,不能分布式,所以
默认是Redis
- 当Redis无法使用或宕机时,自动切换到本地数据源!延迟1小时后,自动切换回
Redis数据源
- 重构Cache帮助器为:
- v1.6
- 新增AOP方式的全局令牌桶速度限制器,包含AOP与拦截器两种方式。
- v1.0:实现
简单使用,只需引入依赖,简单配置一下就能使用,无侵入,易插拔,易使用。
<dependency>
<groupId>cn.sinohealth</groupId>
<artifactId>flow-limit-spring-boot-starter</artifactId>
<version>1.6.0-SNAPSHOT</version>
</dependency>
- counter-flow-limit-properties 与 global-token-bucket-flow-limit-properties:
#配置Redis
spring:
redis:
host: 192.168.16.87
port: 6379
# 配置本启动器
flowlimit:
#是否启用流量限制
enabled: true
counter-flow-limit-properties:
#数据源类型,有redis和local,默认redis
data-source-type: local
#是否启用全局限制,即所有用户所有操作均被一起计数限制.
enabled-global-limit: true
#即计数器的key前缀,可以为空,但不建议
prefix-key: "icecreamtest::innovative-medicine:desktop-web:redis:flow:limit"
#每个计数器的Key,注意计数器的key数量与相应配置值要一致,可以为空,但不建议。
counter-keys:
- "counter:second:3:"
- "counter:minutes:2:"
- "counter:minutes:5:"
- "counter:hour:1:"
- ...
#每个计数器的保持时长,单位是秒
counter-holding-time:
- 6
- 180
- 300
- 3600
- ...
#每个计数器对应的限流次数,即接口调用次数限制
counter-limit-number:
- 5
- 80
- 320
- 240000
- ...
global-token-bucket-flow-limit-properties:
#QPS,即限制一秒钟内最大的请求数量。
permits-per-second: 50
#超时时间,当QPS到达阈值时,允许请求等待的时长
timeout: 10
#预热期时长,单位毫秒
warmup-period: 1000
- AbstractRedisFlowLimitAspect.class
- 经典的计数器,计数器有保持时间,计数上线,当达到阈值时,会触发限流
新建一个类MyRedisFlowLimitConfig继承AbstractRedisFlowLimitAspect抽象类,实现抽象类的方法
//交由Spring托管
@Configuration
//开启切面
@Aspect
public class MyRedisFlowLimitConfig extends AbstractRedisFlowLimitAspect {
//选择需要被限制的Controller方法
@Pointcut("within(cn.sinohealth.flowlimit.springboot.starter.test.TestController)" +
"&&@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void pointcut() {
}
//过滤哪些请求,返回TRUE表示对该请求不进行计数限制
@Override
protected boolean filterRequest(JoinPoint joinPoint) {
if (threadLocal.get().getUseID() **1){
//放行超级管理员
return true;
}
return false;
}
//当计数器达到上限时执行,返回TRUE则清空计数器放行,否则拒绝策略
@Override
protected boolean beforeLimitingHappenWhetherContinueLimit(JoinPoint joinPoint) {
return false;
}
//拒绝策略,可以选择抛出异常,或者返回与Controller类型一样的数据封装
@Override
protected Object rejectHandle(JoinPoint joinPoint) throws Throwable {
response.setCharacterEncoding("utf-8");
response.getWriter().write("接口调用频繁");
response.setStatus(610);
}
//追加用户的ID,enabled-global-limit: true时,会被调用,返回当前登录用户的ID以便限流只是针对当前用户生效。
@Override
protected String appendCounterKeyWithUserId(JoinPoint joinPoint) {
return threadlocal.get().getUserId();
}
}
新建MyRedisFlowLimitInterceptorConfig.class继承AbstractRedisFlowLimitInterceptor并实现其所有方法。
父类已经将拦截器注册了,因此不需要手动在WebMvcConfiguration中注册拦截器,仅仅需要配置拦截路径即可
//交由Spring托管
@Component
public class MyRedisFlowLimitInterceptorConfig extends AbstractRedisFlowLimitInterceptor {
//设置拦截器的拦截路径
@Override
public void setInterceptorPathPatterns(InterceptorRegistration registry) {
registry.addPathPatterns("/api/**");
}
//过滤哪些请求,返回TRUE表示对该请求不进行计数限制
@Override
public boolean filterRequest(HttpServletRequest request, HttpServletResponse response, Object handler) {
return false;
}
//追加用户的ID,enabled-global-limit: true时,会被调用,返回当前登录用户的ID以便限流只是针对当前用户生效。
@Override
public String appendCounterKeyWithUserId(HttpServletRequest request, HttpServletResponse response, Object handler) {
return null;
}
//当计数器达到上限时执行,返回TRUE则清空计数器放行,否则拒绝策略
@Override
public boolean beforeLimitingHappenWhetherContinueLimit(HttpServletRequest request, HttpServletResponse response, Object handler) {
return false;
}
//拒绝策略,可以选择抛出异常,或者返回与Controller类型一样的数据封装
@Override
public void rejectHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setCharacterEncoding("utf-8");
response.getWriter().write("接口调用频繁");
response.setStatus(610);
}
}
新建MyGlobalTokenBucketFlowLimitAspect.class继承AbstractGlobalTokenBucketFlowLimitAspect并实现其所有方法。
@Component
public class MyGlobalTokenBucketFlowLimitAspect extends AbstractGlobalTokenBucketFlowLimitAspect {
//选择需要被限制的Controller方法
@Override
@Pointcut("within(cn.sinohealth.flowlimit.springboot.starter.test.TestController)" +
"&&@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void pointcut() {
}
//过滤哪些请求,返回TRUE表示对该请求不进行限制
@Override
protected boolean filterRequest(JoinPoint obj) {
return false;
}
//拒绝策略,可以选择抛出异常,或者返回与Controller类型一样的数据封装
@Override
protected Object rejectHandle(JoinPoint obj) throws Throwable {
response.setCharacterEncoding("utf-8");
response.getWriter().write("接口调用频繁");
response.setStatus(610);
}
}
新建MyAbstractRedisFlowLimitInterceptor.class继承AbstractRedisFlowLimitInterceptor并实现其所有方法。
@Component
public class MyAbstractRedisFlowLimitInterceptor extends AbstractGlobalTokenBucketFlowLimitInterceptor {
//设置拦截器的拦截路径
@Override
public void setInterceptorPathPatterns(InterceptorRegistration registry) {
registry.addPathPatterns("/**/**");
}
//过滤哪些请求,返回TRUE表示对该请求不进行限制
@Override
public boolean filterRequest(HttpServletRequest request, HttpServletResponse response, Object handler) {
return false;
}
//当计数器达到上限时执行,返回TRUE则清空计数器放行,否则拒绝策略
@Override
public boolean beforeLimitingHappenWhetherContinueLimit(HttpServletRequest request, HttpServletResponse response, Object handler) {
return false;
}
//拒绝策略,可以选择抛出异常,或者返回与Controller类型一样的数据封装
@Override
public Object rejectHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;");
response.getWriter().write("接口调用频繁");
response.setStatus(500);
return handler;
}
}
计数器算法抽象类都有一个公开的方法,resetLimiter();该方法能够重置计数器。
应用场景:当用户请求频繁,后端报错,前端要求用户验证。验证接口可以在Controller中方法,然后调用resetLimiter()方法即可。
public class MyController {
@Autowire
private final RedisFlowLimitInterceptor redisLimitInterceptor;
@Autowire
private final RedisFlowLimitAspect redisLimitAspect;
//RedisFlowLimitAspect的重置,AOP限流方式
@ApiOperation(value = "流量限制器验证操作")
@GetMapping(value = "/pb/sms/verify1")
public BizResponse<Result<SmsSingleSend>> verifyCodeCheckRegister(HttpServletRequest request) {
if (1 == doubleVerify(request)) {
//清空计数器,因为使用的是对象适配器,所以拦截只能这么重置
redisLimitInterceptor.getRedisFlowLimitAspect().resetLimiter(null);
return BizResponse.ok(null);
}
return BizResponse.bizException(BizHttpStatusEnum.VERIFY_CODE_ERROR);
}
//RedisFlowLimitInterceptor,拦截器限流方式
@ApiOperation(value = "流量限制器验证操作")
@GetMapping(value = "/pb/sms/verify2")
public BizResponse<Result<SmsSingleSend>> verifyCodeCheckRegister(HttpServletRequest request) {
if (1 == doubleVerify(request)) {
//清空计数器,AOP限流就比较简单
redisLimitAspect.resetLimiter(null);
return BizResponse.ok(null);
}
return BizResponse.bizException(BizHttpStatusEnum.VERIFY_CODE_ERROR);
}
}
最最最核心的就是,顶级抽象类 AbstractFlowLimit.class
中,使用了模板方法,所有的限流流程都是基于这个抽象类的方法的~
/**
* 定义模板方法,禁止子类重写方法
*/
public final Object flowLimitProcess(T obj)throws Throwable{
if(!enabled){
//未开启限流,或者Redis失效,配置不当
return otherHandle(obj,false,null);//放行
}
if(filterRequest(obj)){
//过滤请求,这个方法是抽象的,必须用户来实现该方法。
return otherHandle(obj,false,null);//放行
}
//限流逻辑
Object rejectResult=null;
boolean isReject=false;
//1.限流操作。
if(limitProcess(obj)){
// 2.限流前置操作
if(beforeLimitingHappenWhetherContinueLimit(obj)){//如果放回TRUE,则重置计数器并取消限流
resetLimiter(obj);
}else{
//执行拒绝策略
isReject=true;
rejectResult=rejectHandle(obj);
}
}
//其他操作,如验证通过重置限制限流器等。最后返回执行结果
//ps:如果isReject为TRUE,则不会放行。
return otherHandle(obj,isReject,rejectResult);
}