前言
本文为描述通过Interceptor以及Redis实现接口访问防刷Demo
这里会通过逐步找问题,逐步去完善的形式展示
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
原理
- 通过ip地址+uri拼接用以作为访问者访问接口区分
- 通过在Interceptor中拦截请求,从Redis中统计用户访问接口次数从而达到接口防刷目的
如下图所示
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
工程
项目地址:
Apifox地址:Apifox 密码:Lyh3j2Rv
其中,Interceptor处代码处理逻辑最为重要
/** * @author: Zero * @time: 2023/2/14 * @description: 接口防刷拦截处理 */ @Slf4j public class AccessLimintInterceptor implements HandlerInterceptor { @Resource private RedisTemplate<String, Object> redisTemplate; /** * 多长时间内 */ @Value("${interfaceAccess.second}") private Long second = 10L; /** * 访问次数 */ @Value("${interfaceAccess.time}") private Long time = 3L; /** * 禁用时长--单位/秒 */ @Value("${interfaceAccess.lockTime}") private Long lockTime = 60L; /** * 锁住时的key前缀 */ public static final String LOCK_PREFIX = "LOCK"; /** * 统计次数时的key前缀 */ public static final String COUNT_PREFIX = "COUNT"; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri = request.getRequestURI(); String ip = request.getRemoteAddr(); // 这里忽略代理软件方式访问,默认直接访问,也就是获取得到的就是访问者真实ip地址 String lockKey = LOCK_PREFIX + ip + uri; Object isLock = redisTemplate.opsForValue().get(lockKey); if(Objects.isNull(isLock)){ // 还未被禁用 String countKey = COUNT_PREFIX + ip + uri; Object count = redisTemplate.opsForValue().get(countKey); if(Objects.isNull(count)){ // 首次访问 log.info("首次访问"); redisTemplate.opsForValue().set(countKey,1,second, TimeUnit.SECONDS); }else{ // 此用户前一点时间就访问过该接口 if((Integer)count < time){ // 放行,访问次数 + 1 redisTemplate.opsForValue().increment(countKey); }else{ log.info("{}禁用访问{}",ip, uri); // 禁用 redisTemplate.opsForValue().set(lockKey, 1,lockTime, TimeUnit.SECONDS); // 删除统计 redisTemplate.delete(countKey); throw new CommonException(ResultCode.ACCESS_FREQUENT); } } }else{ // 此用户访问此接口已被禁用 throw new CommonException(ResultCode.ACCESS_FREQUENT); } return true; } }
在多长时间内访问接口多少次,以及禁用的时长,则是通过与配置文件配合动态设置
当处于禁用时直接抛异常则是通过在ControllerAdvice处统一处理 (这里代码写的有点丑陋)
下面是一些测试(可以把项目通过Git还原到“【初始化】”状态进行测试)
- 正常访问时
- 访问次数过于频繁时
自我提问
上述实现就好像就已经达到了我们的接口防刷目的了
但是,还不够
为方便后续描述,项目中新增补充Controller,如下所示
简单来说就是
PassCotroller和RefuseController- 每个Controller分别有对应的get,post,put,delete类型的方法,其映射路径与方法名称一致
接口自由
- 对于上述实现,不知道你们有没有发现一个问题
- 就是现在我们的接口防刷处理,针对是所有的接口(项目案例中我只是写的接口比较少)
- 而在实际开发中,说对于所有的接口都要做防刷处理,感觉上也不太可能(写此文时目前大四,实际工作经验较少,这里不敢肯定)
- 那么问题有了,该如何解决呢?目前来说想到两个解决方案
拦截器映射规则
项目通过Git还原到"【Interceptor设置映射规则实现接口自由】"版本即可得到此案例实现
我们都知道拦截器是可以设置拦截规则的,从而达到拦截处理目的
1.这个AccessInterfaceInterceptor是专门用来进行防刷处理的,那么实际上我们可以通过设置它的映射规则去匹配需要进行【接口防刷】的接口即可
2.比如说下面的映射配置
3.这样就初步达到了我们的目的,通过映射规则的配置,只针对那些需要进行【接口防刷】的接口才会进行处理
4.至于为啥说是初步呢?下面我就说说目前我想到的使用这种方式进行【接口防刷】的不足点:
所有要进行防刷处理的接口统一都是配置成了 x 秒内 y 次访问次数,禁用时长为 z 秒
- 要知道就是要进行防刷处理的接口,其 x, y, z的值也是并不一定会统一的
- 某些防刷接口处理比较消耗性能的,我就把x, y, z设置的紧一点
- 而某些防刷接口处理相对来说比较快,我就把x, y, z 设置的松一点
- 这没问题吧
- 但是现在呢?x, y, z值全都一致了,这就不行了
- 这就是其中一个不足点
- 当然,其实针对当前这种情况也有解决方案
- 那就是弄多个拦截器
- 每个拦截器的【接口防刷】处理逻辑跟上述一致,并去映射对应要处理的防刷接口
- 唯一不同的就是在每个拦截器内部,去修改对应防刷接口需要的x, y, z值
- 这样就是感觉会比较麻烦
防刷接口映射路径修改后维护问题
- 虽然说防刷接口的映射路径基本上定下来后就不会改变
- 但实际上前后端联调开发项目时,不会有那么严谨的Api文档给我们用(这个在实习中倒是碰到过,公司不是很大,开发起来也就不那么严谨,啥都要自己搞,功能能实现就好)
- 也就是说还是会有那种要修改接口的映射路径需求
- 当防刷接口数量特别多,后面的接手人员就很痛苦了
- 就算是项目是自己从0到1实现的,其实有时候项目开发到后面,自己也会忘记自己前面是如何设计的
- 而使用当前这种方式的话,谁维护谁蛋疼
自定义注解 + 反射
咋说呢
- 就是通过自定义注解中定义 x 秒内 y 次访问次数,禁用时长为 z 秒
- 自定义注解 + 在需要进行防刷处理的各个接口方法上
- 在拦截器中通过反射获取到各个接口中的x, y, z值即可达到我们想要的接口自由目的
下面做个实现
声明自定义注解
Controlller中方法中使用
Interceptor处逻辑修改(最重要是通过反射判断此接口是否需要进行防刷处理,以及获取到x, y, z的值)
/** * @author: Zero * @time: 2023/2/14 * @description: 接口防刷拦截处理 */ @Slf4j public class AccessLimintInterceptor implements HandlerInterceptor { @Resource private RedisTemplate<String, Object> redisTemplate; /** * 锁住时的key前缀 */ public static final String LOCK_PREFIX = "LOCK"; /** * 统计次数时的key前缀 */ public static final String COUNT_PREFIX = "COUNT"; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 自定义注解 + 反射 实现 // 判断访问的是否是接口方法 if(handler instanceof HandlerMethod){ // 访问的是接口方法,转化为待访问的目标方法对象 HandlerMethod targetMethod = (HandlerMethod) handler; // 取出目标方法中的 AccessLimit 注解 AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class); // 判断此方法接口是否要进行防刷处理(方法上没有对应注解就代表不需要,不需要的话进行放行) if(!Objects.isNull(accessLimit)){ // 需要进行防刷处理,接下来是处理逻辑 String ip = request.getRemoteAddr(); String uri = request.getRequestURI(); String lockKey = LOCK_PREFIX + ip + uri; Object isLock = redisTemplate.opsForValue().get(lockKey); // 判断此ip用户访问此接口是否已经被禁用 if (Objects.isNull(isLock)) { // 还未被禁用 String countKey = COUNT_PREFIX + ip + uri; Object count = redisTemplate.opsForValue().get(countKey); long second = accessLimit.second(); long maxTime = accessLimit.maxTime(); if (Objects.isNull(count)) { // 首次访问 log.info("首次访问"); redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS); } else { // 此用户前一点时间就访问过该接口,且频率没超过设置 if ((Integer) count < maxTime) { redisTemplate.opsForValue().increment(countKey); } else { log.info("{}禁用访问{}", ip, uri); long forbiddenTime = accessLimit.forbiddenTime(); // 禁用 redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS); // 删除统计--已经禁用了就没必要存在了 redisTemplate.delete(countKey); throw new CommonException(ResultCode.ACCESS_FREQUENT); } } } else { // 此用户访问此接口已被禁用 throw new CommonException(ResultCode.ACCESS_FREQUENT); } } } return true; } }
由于不好演示效果,这里就不贴测试结果图片了
项目通过Git还原到"【自定义主键+反射实现接口自由"版本即可得到此案例实现,后面自己可以针对接口做下测试看看是否如同我所说的那样实现自定义x, y, z 的效果
嗯,现在看起来,可以针对每个要进行防刷处理的接口进行针对性自定义多长时间内的最大访问次数,以及禁用时长,哪个接口需要,就直接+在那个接口方法出即可
感觉还不错的样子,现在网上挺多资料也都是这样实现的
但是还是可以有改善的地方
先举一个例子,以我们的PassController为例,如下是其实现
下图是其映射路径关系














