SpringBoot定义拦截器+自定义注解+Redis实现接口防刷(限流) ...

打印 上一主题 下一主题

主题 532|帖子 532|积分 1596


  • 实现思路

    • 在拦截器Interceptor中拦截请求
    • 通过地址+请求uri作为调用者访问接口的区分在Redis中进行计数达到限流目的

  • 简单实现

    • 定义参数

      • 访问周期
      • 最大访问次数
      • 禁用时长
      1. #接口防刷配置,时间单位都是秒.  如果second秒内访问次数达到times,就禁用lockTime秒
      2. access:
      3.   limit:
      4.     second: 10 #一段时间内
      5.     times: 3  #最大访问次数
      6.     lockTime: 5 #禁用时长
      复制代码
    • 代码实现

      • 定义拦截器:实现HandlerInterceptor接口,重写preHandle()方法
        1. @Slf4j
        2. @Component
        3. public class AccessLimintInterceptor implements HandlerInterceptor {
        4.    
        5.     @Resource
        6.     private RedisTemplate redisTemplate;
        7.     //锁住时的key前缀
        8.     private static final String LOCK_PREFIX = "LOCK";
        9.     //统计次数的key前缀
        10.     private static final String COUNT_PREFIX = "COUNT";
        11.     //访问周期
        12.     @Value("${access.limit.second}")
        13.     private long second;
        14.     //访问周期内最大访问次数
        15.     @Value("${access.limit.times}")
        16.     private int times;
        17.     //禁用时长
        18.     @Value("${access.limit.lockTime}")
        19.     private long lockTime;
        20.    
        21.     @Override
        22.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        23.         return true;
        24.     }
        复制代码
      • 注册拦截器:配置类实现WebMvcConfigurer接口,重写addInterceptors()方法
        1. @Configuration
        2. public class WebConfig implements WebMvcConfigurer {
        3.     @Resource
        4.     private AccessLimintInterceptor accessLimintInterceptor;
        5.     //在这个方法中注册拦截器
        6.     @Override
        7.     public void addInterceptors(InterceptorRegistry registry) {
        8.         //注册拦截器
        9.         InterceptorRegistration interceptorRegistration = registry.addInterceptor(accessLimintInterceptor);
        10.         //配置要拦截的路径。优化为实现自定义注解,那就拦截所有路径,在拦截器中判断是否使用了注解,没使用就放行
        11. //        interceptorRegistration.addPathPatterns("/search/**");
        12.         interceptorRegistration.addPathPatterns("/**");
        13.         WebMvcConfigurer.super.addInterceptors(registry);
        14.     }
        15. }
        复制代码
      • 自定义异常,方便错误提示。
        1. /*
        2. * @Description TODO (自定义访问限制异常,防刷)
        3. * 创建人: 程长新
        4. * 创建时间:2023/11/12 8:46
        5. **/
        6. public class AccessLimitException extends RuntimeException{
        7.     public AccessLimitException() {
        8.     }
        9.     public AccessLimitException(Throwable e) {
        10.         super(e.getMessage(),e);
        11.     }
        12.     public AccessLimitException(String message) {
        13.         super(message);
        14.     }
        15. }
        复制代码
        添加全局异常捕捉
        1. /*
        2. * @Description TODO (全局异常处理)
        3. * 创建人: 程长新
        4. * 创建时间:2023/11/7 9:54
        5. **/
        6. @RestControllerAdvice
        7. public class AdviceController {
        8.     @ExceptionHandler(Exception.class)
        9.     public String exceptionHandler(HttpServletRequest request,
        10.                                    HttpServletResponse response,
        11.                                    Exception e){
        12.         return e.getMessage();
        13.     }
        14.     @ExceptionHandler(AccessLimitException.class)
        15.     public String exceptionHandler(AccessLimitException e){
        16.         return "访问次数过多,请稍候再试";
        17.     }
        18. }
        复制代码
      • 处理逻辑
        1. /** 不使用自定义注解时的逻辑
        2. *获取锁key
        3. *  1 锁key为空,未被禁用,进入处理逻辑
        4. *      获取计数key
        5. *          1)计数key为空,说明首次访问,设置计数key为1,放行
        6. *          2)计数key不为空,判断是否达到最大访问次数
        7. *              (1)达到:返回错误提示
        8. *              (2)未达到:计数值+1
        9. *  2 锁key不为空,已被禁用,直接返回提示
        10. */
        11. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        12.     log.info("进入拦截器");
        13.     //获取访问的url和访问者ip
        14.     String requestURI = request.getRequestURI();
        15.     String remoteAddr = request.getRemoteAddr();
        16.     String lockKey = LOCK_PREFIX + requestURI + remoteAddr;
        17.     Object o = redisTemplate.opsForValue().get(lockKey);
        18.     if (Objects.isNull(o)){
        19.         //还未被禁用
        20.         //查看当前访问次数
        21.         String countKey = COUNT_PREFIX + requestURI + remoteAddr;
        22.         Integer count = (Integer)redisTemplate.opsForValue().get(countKey);
        23.         if (Objects.isNull(count)){
        24.             //首次访问
        25.             log.info("{}用户首次访问接口{}",remoteAddr,requestURI);
        26.             redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
        27.             log.info("访问次数写入redis");
        28.         }else {
        29.             log.info("{}用户第{}次访问接口{}", remoteAddr, count + 1, requestURI);
        30.             //此用户在设置的一段时间内已经访问过该接口
        31.             //判断次数+1是否超过最大限制
        32.             if (count++ >= times){
        33.                 //超过最大限制,禁用该用户对此接口的访问
        34.                 log.info("{}用户访问接口{}已达到最大限制,禁用",remoteAddr,requestURI);
        35.                 redisTemplate.opsForValue().set(lockKey, 1, lockTime, TimeUnit.SECONDS);
        36.                 //返回提示
        37.                 //                    throw new RuntimeException("服务器繁忙,请稍候再试");
        38.                 throw new AccessLimitException();
        39.             }else {
        40.                 //访问次数+1
        41.                 ValueOperations valueOperations = redisTemplate.opsForValue();
        42.                 valueOperations.set(countKey, count, second, TimeUnit.SECONDS);
        43.             }
        44.         }
        45.     }else {
        46.         //已被禁用,返回提示
        47.         throw new AccessLimitException();
        48.     }
        49.     return true;
        50. }
        复制代码
      • 目前存在的问题

        此时已经简单实现了限流功能,但是上边配置拦截路径直接写了/**,是为了方便测试,但是如果正常开发应该不会写全部,应该单个配置,那么就要为每个接口添加配置,比较繁琐。并且现在对所有接口的限制都是一样的规则,时间都是一样的,如果想要有不同的时间规则,那么就需要设置多个过滤器,明显是不合适的,所以需要优化。


  • 优化一:自定义注解+反射

    • 定义注解
      1. /*
      2. * @Description TODO (自定义接口防刷注解)
      3. * 创建人: 程长新
      4. * 创建时间:2023/11/12 9:03
      5. **/
      6. @Target({ElementType.METHOD})//注解可以作用在方法上
      7. @Retention(RetentionPolicy.RUNTIME)
      8. @Documented
      9. public @interface AccessLimit {
      10.     /**
      11.      * 时间周期
      12.      */
      13.     long second() default 5L;
      14.     /**
      15.      * 最大访问次数
      16.      */
      17.     int times() default 3;
      18.     /**
      19.      * 禁用时长
      20.      */
      21.     long lockTime() default 3L;
      22. }
      复制代码
    • 将注解标注写需要限流的方法上
      1. @AccessLimit(second = 10L, times = 5, lockTime = 2L)
      2. @GetMapping("/search")
      3. public String search(){
      4.     return "进来了";
      5. }
      复制代码
    • 修改处理逻辑

      主要修改:通过反射获取到方法注解,判断是否需要进行限流,如果需要就获取注解中的参数进行处理
      1. @Override
      2. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      3.     log.info("进入拦截器");
      4.     //判断拦截的是否为接口方法
      5.     if (handler instanceof HandlerMethod){
      6.         log.info("开始处理");
      7.         //转化为目标方法对象
      8.         HandlerMethod targetMethod = (HandlerMethod) handler;
      9.         //获取对象的AccessLimit注解
      10.         AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
      11.         //如果获取到注解再进行处理,否则直接放行
      12.         if(Objects.nonNull(accessLimit)){
      13.             //防刷处理逻辑
      14.             //获取访问的接口的访问者IP
      15.             String remoteAddr = request.getRemoteAddr();
      16.             String requestURI = request.getRequestURI();
      17.             //拼接锁key和计数key
      18.             String lockKey = LOCK_PREFIX + requestURI + remoteAddr;
      19.             String countKey = COUNT_PREFIX + requestURI + remoteAddr;
      20.             //从redis中获取锁值
      21.             Object o = redisTemplate.opsForValue().get(lockKey);
      22.             if (Objects.nonNull(o)){
      23.                 log.info("用户{},访问{}接口,被禁用",remoteAddr,requestURI);
      24.                 //获取锁值不为空说明已经禁用,直接返回
      25.                 throw new AccessLimitException();
      26.             }else {
      27.                 //未被禁用
      28.                 //获取注解中设置的x,y,z时间值
      29.                 long second1 = accessLimit.second();
      30.                 int times1 = accessLimit.times();
      31.                 long lockTime1 = accessLimit.lockTime();
      32.                 //获取访问次数
      33.                 Integer o1 = (Integer) redisTemplate.opsForValue().get(countKey);
      34.                 if (Objects.isNull(o1)){
      35.                     log.info("用户{},访问{}接口,首次访问",remoteAddr,requestURI);
      36.                     //首次访问,保存访问次数为1
      37.                     redisTemplate.opsForValue().set(countKey,1,second1,TimeUnit.SECONDS);
      38.                 }else {
      39.                     //判断访问次数
      40.                     if (o1 == times1){
      41.                         log.info("用户{},访问{}接口,达到次数限制被禁用",remoteAddr,requestURI);
      42.                         //已经达到限制,禁用,返回
      43.                         redisTemplate.opsForValue().set(lockKey,1,lockTime1,TimeUnit.SECONDS);
      44.                         //删除计数key,已经禁用,这个也就没必要了
      45.                         redisTemplate.delete(countKey);
      46.                         throw new AccessLimitException();
      47.                     }else {
      48.                         log.info("用户{},访问{}接口,现在第{}次访问",remoteAddr,requestURI,(o1 + 1));
      49.                         //次数加1
      50.                         redisTemplate.opsForValue().set(countKey,++o1,second1,TimeUnit.SECONDS);
      51.                     }
      52.                 }
      53.             }
      54.         }
      55.     }
      56.     return true;
      57. }
      复制代码
    • 目前存在的问题

      对需要进行限流的每个方法得挨个添加注解,那么如果一个controller中的所以接口都需要限流处理的话,每个接口挨个添加注解的做法属实不怎么样。应该做到如果在一个controller上添加了注解,那么这个controller中的所以接口都进行限流,如果某个接口上也添加了注解,那么就采用就近原则使用接口上注解的参数。仍然需要优化

  • 优化二:注解作用于类上

    • 添加注解作用范围
      1. @Target({ElementType.METHOD, ElementType.TYPE})//添加ElementType.TYPE范围
      2. @Retention(RetentionPolicy.RUNTIME)
      3. @Documented
      4. public @interface AccessLimit {
      5.     /**
      6.      * 时间周期
      7.      */
      8.     long second() default 5L;
      9.     /**
      10.      * 最大访问次数
      11.      */
      12.     int times() default 3;
      13.     /**
      14.      * 禁用时长
      15.      */
      16.     long lockTime() default 3L;
      17. }
      复制代码
    • 修改处理逻辑
      1. /**自定义注解可以作用在类上之后的逻辑
      2. * 1 获取类上的注解
      3. * 2 获取方法上的注解
      4. * 3 判断类是是否有注解
      5. *   1)类上没有
      6. *     判断方法上是否存在注解
      7. *       不存在:说明该接口不需要防刷,放行就可以
      8. *       存在:获取注解中的值,进行处理
      9. *   2)类上存在注解
      10. *     判断方法上是否存在注解
      11. *       不存在:说明该方法使用类上的统一配置
      12. *       存在:采用就近原则,使用方法上注解的值进行处理
      13. */
      14. @Override
      15. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      16.     //判断拦截的是否为接口方法
      17.     if (handler instanceof HandlerMethod){
      18.         //转化为目标方法
      19.         HandlerMethod targetMethod = (HandlerMethod) handler;
      20.         //获取目标类上的注解
      21.         //不可以直接使用targetMethod.getClass(),这样获取到的是HandlerMethod,不是真正想要的controller类
      22.         //            Class<? extends HandlerMethod> aClass = targetMethod.getClass();
      23.         Class<?> targetClass = targetMethod.getMethod().getDeclaringClass();
      24.         AccessLimit classAccessLimit = targetClass.getAnnotation(AccessLimit.class);
      25.         //获取目标方法上的注解,
      26.         AccessLimit methodAccessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
      27.         //类名#方法名[参数个数]
      28.         String shortLogMessage = targetMethod.getShortLogMessage();
      29.         long second = 0L;//一段时间内
      30.         int times = 0;//最大访问次数
      31.         long lockTime = 0L;//禁用时长
      32.         if (Objects.nonNull(classAccessLimit)){
      33.             //类上存在注解
      34.             if (Objects.nonNull(methodAccessLimit)){
      35.                 //方法上存在注解,就近原则,使用方法上注解的参数
      36.                 second = methodAccessLimit.second();
      37.                 times = methodAccessLimit.times();
      38.                 lockTime = methodAccessLimit.lockTime();
      39.             }else {
      40.                 second = classAccessLimit.second();
      41.                 times = classAccessLimit.times();
      42.                 lockTime = classAccessLimit.lockTime();
      43.             }
      44.             //只传uri的话,如果请求中含有路径参数,那么请求同一个接口但传递不同参数也会记录为不同的key,就会导致防刷失效,所以将uri改为类名+方法名
      45.             if(isLimit(second, times, lockTime, request.getRemoteAddr(), shortLogMessage)){
      46.                 throw new AccessLimitException();
      47.             }
      48.         }else {
      49.             //类上不存在注解
      50.             //判断方法上是否存在
      51.             if (Objects.nonNull(methodAccessLimit)){
      52.                 //方法上存在注解
      53.                 second = methodAccessLimit.second();
      54.                 times = methodAccessLimit.times();
      55.                 lockTime = methodAccessLimit.lockTime();
      56.                 if(isLimit(second, times, lockTime, request.getRemoteAddr(), shortLogMessage)){
      57.                     throw new AccessLimitException();
      58.                 }
      59.             }
      60.             //方法上不存在,不用分支了,直接到最后return true
      61.         }
      62.     }
      63.     return true;
      64. }
      65. /**
      66. * 判断该ip访问此uri是否已经被限制
      67. * @param second
      68. * @param times
      69. * @param lockTime
      70. * @param ip
      71. * @param uri 请求的接口名:类名#方法名[参数个数]
      72. * @return  true:禁用 false:未禁用
      73. */
      74. public boolean isLimit(long second, int times, long lockTime, String ip, String uri){
      75.     String lockKey = LOCK_PREFIX + ip + uri;
      76.     String countKey = COUNT_PREFIX + ip + uri;
      77.     Object o = redisTemplate.opsForValue().get(lockKey);
      78.     if (Objects.nonNull(o)){
      79.         log.info("用户{},访问{}接口,被禁用",ip,uri);
      80.         //获取锁值不为空说明已经禁用,直接返回
      81.         return true;
      82.     }else {
      83.         //未被禁用
      84.         //获取访问次数
      85.         Integer o1 = (Integer) redisTemplate.opsForValue().get(countKey);
      86.         if (Objects.isNull(o1)){
      87.             log.info("用户{},访问{}接口,首次访问",ip,uri);
      88.             //首次访问,保存访问次数为1
      89.             redisTemplate.opsForValue().set(countKey,1,second,TimeUnit.SECONDS);
      90.         }else {
      91.             //判断访问次数
      92.             if (o1 == times){
      93.                 log.info("用户{},访问{}接口,达到次数限制被禁用",ip,uri);
      94.                 //已经达到限制,禁用,返回
      95.                 redisTemplate.opsForValue().set(lockKey,1,lockTime,TimeUnit.SECONDS);
      96.                 //删除计数key,已经禁用,这个也就没必要了
      97.                 redisTemplate.delete(countKey);
      98.                 return true;
      99.             }else {
      100.                 log.info("用户{},访问{}接口,现在第{}次访问",ip,uri,(o1 + 1));
      101.                 //次数加1
      102.                 //                    redisTemplate.opsForValue().set(countKey,++o1,second,TimeUnit.SECONDS);
      103.                 Long increment = redisTemplate.opsForValue().increment(countKey);
      104.             }
      105.         }
      106.     }
      107.     return false;
      108. }
      复制代码
    • 到此限流方案完善


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

干翻全岛蛙蛙

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表