如何实现接口防刷

前言

原理



其中,Interceptor处代码处理逻辑最为重要

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor  implements HandlerInterceptor {
    @Resource
    private RedisTemplate 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;
    }
}
复制代码

自我提问

接口自由

拦截器映射规则

我们都知道拦截器是可以设置拦截规则的,从而达到拦截处理目的

这个AccessInterfaceInterceptor是专门用来进行防刷处理的,那么实际上我们可以通过设置它的映射规则去匹配需要进行【接口防刷】的接口即可

比如说下面的映射配置

这样就初步达到了我们的目的,通过映射规则的配置,只针对那些需要进行【接口防刷】的接口才会进行处理

至于为啥说是初步呢?下面我就说说目前我想到的使用这种方式进行【接口防刷】的不足点:

所有要进行防刷处理的接口统一都是配置成了 x 秒内 y 次访问次数,禁用时长为 z 秒

要知道就是要进行防刷处理的接口,其 x, y, z的值也是并不一定会统一的

某些防刷接口处理比较消耗性能的,我就把x, y, z设置的紧一点

而某些防刷接口处理相对来说比较快,我就把x, y, z 设置的松一点

这没问题吧

但是现在呢?x, y, z值全都一致了,这就不行了

这就是其中一个不足点

当然,其实针对当前这种情况也有解决方案

那就是弄多个拦截器

每个拦截器的【接口防刷】处理逻辑跟上述一致,并去映射对应要处理的防刷接口

唯一不同的就是在每个拦截器内部,去修改对应防刷接口需要的x, y, z值

这样就是感觉会比较麻烦

防刷接口映射路径修改后维护问题

虽然说防刷接口的映射路径基本上定下来后就不会改变

但实际上前后端联调开发项目时,不会有那么严谨的Api文档给我们用(这个在实习中倒是碰到过,公司不是很大,开发起来也就不那么严谨,啥都要自己搞,功能能实现就好)

也就是说还是会有那种要修改接口的映射路径需求

当防刷接口数量特别多,后面的接手人员就很痛苦了

就算是项目是自己从0到1实现的,其实有时候项目开发到后面,自己也会忘记自己前面是如何设计的

而使用当前这种方式的话,谁维护谁蛋疼

自定义注解 + 反射


/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor  implements HandlerInterceptor {
    @Resource
    private RedisTemplate 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;
    }
}

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate 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 {
​
//      自定义注解 + 反射 实现, 版本 2.0
        if (handler instanceof HandlerMethod) {
            // 访问的是接口方法,转化为待访问的目标方法对象
            HandlerMethod targetMethod = (HandlerMethod) handler;
            // 获取目标接口方法所在类的注解@AccessLimit
            AccessLimit targetClassAnnotation = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);
            // 特别注意不能采用下面这条语句来获取,因为 Spring 采用的代理方式来代理目标方法
            //  也就是说targetMethod.getClass()获得是class org.springframework.web.method.HandlerMethod ,而不知我们真正想要的 Controller
//            AccessLimit targetClassAnnotation = targetMethod.getClass().getAnnotation(AccessLimit.class);
            // 定义标记位,标记此类是否加了@AccessLimit注解
            boolean isBrushForAllInterface = false;
            String ip = request.getRemoteAddr();
            String uri = request.getRequestURI();
            long second = 0L;
            long maxTime = 0L;
            long forbiddenTime = 0L;
            if (!Objects.isNull(targetClassAnnotation)) {
                log.info("目标接口方法所在类上有@AccessLimit注解");
                isBrushForAllInterface = true;
                second = targetClassAnnotation.second();
                maxTime = targetClassAnnotation.maxTime();
                forbiddenTime = targetClassAnnotation.forbiddenTime();
            }
            // 取出目标方法中的 AccessLimit 注解
            AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
            // 判断此方法接口是否要进行防刷处理
            if (!Objects.isNull(accessLimit)) {
                // 需要进行防刷处理,接下来是处理逻辑
                second = accessLimit.second();
                maxTime = accessLimit.maxTime();
                forbiddenTime = accessLimit.forbiddenTime();
                if (isForbindden(second, maxTime, forbiddenTime, ip, uri)) {
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                }
            } else {
                // 目标接口方法处无@AccessLimit注解,但还要看看其类上是否加了(类上有加,代表针对此类下所有接口方法都要进行防刷处理)
                if (isBrushForAllInterface && isForbindden(second, maxTime, forbiddenTime, ip, uri)) {
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                }
            }
        }
        return true;
    }
​
    /**
     * 判断某用户访问某接口是否已经被禁用/是否需要禁用
     *
     * @param second        多长时间  单位/秒
     * @param maxTime       最大访问次数
     * @param forbiddenTime 禁用时长 单位/秒
     * @param ip            访问者ip地址
     * @param uri           访问的uri
     * @return ture为需要禁用
     */
    private boolean isForbindden(long second, long maxTime, long forbiddenTime, String ip, String uri) {
        String lockKey = LOCK_PREFIX + ip + uri; //如果此ip访问此uri被禁用时的存在Redis中的 key
        Object isLock = redisTemplate.opsForValue().get(lockKey);
        // 判断此ip用户访问此接口是否已经被禁用
        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 < maxTime) {
                    redisTemplate.opsForValue().increment(countKey);
                } else {
                    log.info("{}禁用访问{}", ip, uri);
                    // 禁用
                    redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                    // 删除统计--已经禁用了就没必要存在了
                    redisTemplate.delete(countKey);
                    return true;
                }
            }
        } else {
            // 此用户访问此接口已被禁用
            return true;
        }
        return false;
    }
}



时间逻辑漏洞

路径参数问题

这算是比较理想的做法,相当于没这个问题

但有一定局限性,有时候接手别的项目,或者自己根本没这个权限说不能使用路径参数

我们获取uri的目的,其实就是为了区别访问接口

而把uri替换成另一种可以区分访问接口方法的标识即可

最容易想到的就是通过反射获取到接口方法名称,使用接口方法名称替换成uri即可

当然,其实不同的Controller中,其接口方法名称也有可能是相同的

实际上可以再获取接口方法所在类类名,使用类名 + 方法名称替换uri即可

实际解决方案有很多,看个人需求吧

真实ip获取

总结



展开阅读全文

页面更新:2024-05-04

标签:接口   目的   前缀   注解   反射   路径   逻辑   次数   时间   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top