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

这篇具有很好参考价值的文章主要介绍了SpringBoot定义拦截器+自定义注解+Redis实现接口防刷(限流)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  1. 实现思路

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

    1. 定义参数

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

      1. 定义拦截器:实现HandlerInterceptor接口,重写preHandle()方法
        @Slf4j
        @Component
        public class AccessLimintInterceptor implements HandlerInterceptor {
            
            @Resource
            private RedisTemplate redisTemplate;
        
            //锁住时的key前缀
            private static final String LOCK_PREFIX = "LOCK";
            //统计次数的key前缀
            private static final String COUNT_PREFIX = "COUNT";
        
            //访问周期
            @Value("${access.limit.second}")
            private long second;
            //访问周期内最大访问次数
            @Value("${access.limit.times}")
            private int times;
            //禁用时长
            @Value("${access.limit.lockTime}")
            private long lockTime;
            
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                return true;
            }
        
      2. 注册拦截器:配置类实现WebMvcConfigurer接口,重写addInterceptors()方法
        @Configuration
        public class WebConfig implements WebMvcConfigurer {
            @Resource
            private AccessLimintInterceptor accessLimintInterceptor;
        
            //在这个方法中注册拦截器
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                //注册拦截器
                InterceptorRegistration interceptorRegistration = registry.addInterceptor(accessLimintInterceptor);
                //配置要拦截的路径。优化为实现自定义注解,那就拦截所有路径,在拦截器中判断是否使用了注解,没使用就放行
        //        interceptorRegistration.addPathPatterns("/search/**");
                interceptorRegistration.addPathPatterns("/**");
                WebMvcConfigurer.super.addInterceptors(registry);
            }
        }
        
      3. 自定义异常,方便错误提示。
        /*
         * @Description TODO (自定义访问限制异常,防刷)
         * 创建人: 程长新
         * 创建时间:2023/11/12 8:46
         **/
        public class AccessLimitException extends RuntimeException{
            public AccessLimitException() {
            }
        
            public AccessLimitException(Throwable e) {
                super(e.getMessage(),e);
            }
        
            public AccessLimitException(String message) {
                super(message);
            }
        }
        

        添加全局异常捕捉

        /*
         * @Description TODO (全局异常处理)
         * 创建人: 程长新
         * 创建时间:2023/11/7 9:54
         **/
        @RestControllerAdvice
        public class AdviceController {
            @ExceptionHandler(Exception.class)
            public String exceptionHandler(HttpServletRequest request,
                                           HttpServletResponse response,
                                           Exception e){
                return e.getMessage();
            }
        
            @ExceptionHandler(AccessLimitException.class)
            public String exceptionHandler(AccessLimitException e){
                return "访问次数过多,请稍候再试";
            }
        }
        
      4. 处理逻辑
        /** 不使用自定义注解时的逻辑
        *获取锁key
        *  1 锁key为空,未被禁用,进入处理逻辑
        *      获取计数key
        *          1)计数key为空,说明首次访问,设置计数key为1,放行
        *          2)计数key不为空,判断是否达到最大访问次数
        *              (1)达到:返回错误提示
        *              (2)未达到:计数值+1
        *  2 锁key不为空,已被禁用,直接返回提示
        */
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            log.info("进入拦截器");
            //获取访问的url和访问者ip
            String requestURI = request.getRequestURI();
            String remoteAddr = request.getRemoteAddr();
            String lockKey = LOCK_PREFIX + requestURI + remoteAddr;
            Object o = redisTemplate.opsForValue().get(lockKey);
            if (Objects.isNull(o)){
                //还未被禁用
                //查看当前访问次数
                String countKey = COUNT_PREFIX + requestURI + remoteAddr;
                Integer count = (Integer)redisTemplate.opsForValue().get(countKey);
                if (Objects.isNull(count)){
                    //首次访问
                    log.info("{}用户首次访问接口{}",remoteAddr,requestURI);
                    redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
                    log.info("访问次数写入redis");
                }else {
                    log.info("{}用户第{}次访问接口{}", remoteAddr, count + 1, requestURI);
                    //此用户在设置的一段时间内已经访问过该接口
                    //判断次数+1是否超过最大限制
                    if (count++ >= times){
                        //超过最大限制,禁用该用户对此接口的访问
                        log.info("{}用户访问接口{}已达到最大限制,禁用",remoteAddr,requestURI);
                        redisTemplate.opsForValue().set(lockKey, 1, lockTime, TimeUnit.SECONDS);
                        //返回提示
                        //                    throw new RuntimeException("服务器繁忙,请稍候再试");
                        throw new AccessLimitException();
                    }else {
                        //访问次数+1
                        ValueOperations valueOperations = redisTemplate.opsForValue();
                        valueOperations.set(countKey, count, second, TimeUnit.SECONDS);
                    }
                }
            }else {
                //已被禁用,返回提示
                throw new AccessLimitException();
            }
            return true;
        }
        
      5. 目前存在的问题

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

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

    1. 定义注解

      /*
       * @Description TODO (自定义接口防刷注解)
       * 创建人: 程长新
       * 创建时间:2023/11/12 9:03
       **/
      @Target({ElementType.METHOD})//注解可以作用在方法上
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface AccessLimit {
          /**
           * 时间周期
           */
          long second() default 5L;
      
          /**
           * 最大访问次数
           */
          int times() default 3;
      
          /**
           * 禁用时长
           */
          long lockTime() default 3L;
      }
      
    2. 将注解标注写需要限流的方法上

      @AccessLimit(second = 10L, times = 5, lockTime = 2L)
      @GetMapping("/search")
      public String search(){
          return "进来了";
      }
      
    3. 修改处理逻辑

      主要修改:通过反射获取到方法注解,判断是否需要进行限流,如果需要就获取注解中的参数进行处理

      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
          log.info("进入拦截器");
          //判断拦截的是否为接口方法
          if (handler instanceof HandlerMethod){
              log.info("开始处理");
              //转化为目标方法对象
              HandlerMethod targetMethod = (HandlerMethod) handler;
              //获取对象的AccessLimit注解
              AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
              //如果获取到注解再进行处理,否则直接放行
              if(Objects.nonNull(accessLimit)){
                  //防刷处理逻辑
                  //获取访问的接口的访问者IP
                  String remoteAddr = request.getRemoteAddr();
                  String requestURI = request.getRequestURI();
                  //拼接锁key和计数key
                  String lockKey = LOCK_PREFIX + requestURI + remoteAddr;
                  String countKey = COUNT_PREFIX + requestURI + remoteAddr;
                  //从redis中获取锁值
                  Object o = redisTemplate.opsForValue().get(lockKey);
                  if (Objects.nonNull(o)){
                      log.info("用户{},访问{}接口,被禁用",remoteAddr,requestURI);
                      //获取锁值不为空说明已经禁用,直接返回
                      throw new AccessLimitException();
                  }else {
                      //未被禁用
                      //获取注解中设置的x,y,z时间值
                      long second1 = accessLimit.second();
                      int times1 = accessLimit.times();
                      long lockTime1 = accessLimit.lockTime();
                      //获取访问次数
                      Integer o1 = (Integer) redisTemplate.opsForValue().get(countKey);
                      if (Objects.isNull(o1)){
                          log.info("用户{},访问{}接口,首次访问",remoteAddr,requestURI);
                          //首次访问,保存访问次数为1
                          redisTemplate.opsForValue().set(countKey,1,second1,TimeUnit.SECONDS);
                      }else {
                          //判断访问次数
                          if (o1 == times1){
                              log.info("用户{},访问{}接口,达到次数限制被禁用",remoteAddr,requestURI);
                              //已经达到限制,禁用,返回
                              redisTemplate.opsForValue().set(lockKey,1,lockTime1,TimeUnit.SECONDS);
                              //删除计数key,已经禁用,这个也就没必要了
                              redisTemplate.delete(countKey);
                              throw new AccessLimitException();
                          }else {
                              log.info("用户{},访问{}接口,现在第{}次访问",remoteAddr,requestURI,(o1 + 1));
                              //次数加1
                              redisTemplate.opsForValue().set(countKey,++o1,second1,TimeUnit.SECONDS);
                          }
                      }
                  }
              }
          }
          return true;
      }
      
    4. 目前存在的问题

      对需要进行限流的每个方法得挨个添加注解,那么如果一个controller中的所以接口都需要限流处理的话,每个接口挨个添加注解的做法属实不怎么样。应该做到如果在一个controller上添加了注解,那么这个controller中的所以接口都进行限流,如果某个接口上也添加了注解,那么就采用就近原则使用接口上注解的参数。仍然需要优化文章来源地址https://www.toymoban.com/news/detail-746188.html

  4. 优化二:注解作用于类上

    1. 添加注解作用范围

      @Target({ElementType.METHOD, ElementType.TYPE})//添加ElementType.TYPE范围
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface AccessLimit {
          /**
           * 时间周期
           */
          long second() default 5L;
      
          /**
           * 最大访问次数
           */
          int times() default 3;
      
          /**
           * 禁用时长
           */
          long lockTime() default 3L;
      }
      
    2. 修改处理逻辑

      /**自定义注解可以作用在类上之后的逻辑
      * 1 获取类上的注解
      * 2 获取方法上的注解
      * 3 判断类是是否有注解
      *   1)类上没有
      *     判断方法上是否存在注解
      *       不存在:说明该接口不需要防刷,放行就可以
      *       存在:获取注解中的值,进行处理
      *   2)类上存在注解
      *     判断方法上是否存在注解
      *       不存在:说明该方法使用类上的统一配置
      *       存在:采用就近原则,使用方法上注解的值进行处理
      */
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
          //判断拦截的是否为接口方法
          if (handler instanceof HandlerMethod){
              //转化为目标方法
              HandlerMethod targetMethod = (HandlerMethod) handler;
              //获取目标类上的注解
              //不可以直接使用targetMethod.getClass(),这样获取到的是HandlerMethod,不是真正想要的controller类
              //            Class<? extends HandlerMethod> aClass = targetMethod.getClass();
              Class<?> targetClass = targetMethod.getMethod().getDeclaringClass();
              AccessLimit classAccessLimit = targetClass.getAnnotation(AccessLimit.class);
              //获取目标方法上的注解,
              AccessLimit methodAccessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
              //类名#方法名[参数个数]
              String shortLogMessage = targetMethod.getShortLogMessage();
              long second = 0L;//一段时间内
              int times = 0;//最大访问次数
              long lockTime = 0L;//禁用时长
              if (Objects.nonNull(classAccessLimit)){
                  //类上存在注解
                  if (Objects.nonNull(methodAccessLimit)){
                      //方法上存在注解,就近原则,使用方法上注解的参数
                      second = methodAccessLimit.second();
                      times = methodAccessLimit.times();
                      lockTime = methodAccessLimit.lockTime();
                  }else {
                      second = classAccessLimit.second();
                      times = classAccessLimit.times();
                      lockTime = classAccessLimit.lockTime();
                  }
                  //只传uri的话,如果请求中含有路径参数,那么请求同一个接口但传递不同参数也会记录为不同的key,就会导致防刷失效,所以将uri改为类名+方法名
                  if(isLimit(second, times, lockTime, request.getRemoteAddr(), shortLogMessage)){
                      throw new AccessLimitException();
                  }
              }else {
                  //类上不存在注解
                  //判断方法上是否存在
                  if (Objects.nonNull(methodAccessLimit)){
                      //方法上存在注解
                      second = methodAccessLimit.second();
                      times = methodAccessLimit.times();
                      lockTime = methodAccessLimit.lockTime();
                      if(isLimit(second, times, lockTime, request.getRemoteAddr(), shortLogMessage)){
                          throw new AccessLimitException();
                      }
                  }
                  //方法上不存在,不用分支了,直接到最后return true
              }
          }
          return true;
      }
      
      /**
      * 判断该ip访问此uri是否已经被限制
      * @param second
      * @param times
      * @param lockTime
      * @param ip
      * @param uri 请求的接口名:类名#方法名[参数个数]
      * @return  true:禁用 false:未禁用
      */
      public boolean isLimit(long second, int times, long lockTime, String ip, String uri){
          String lockKey = LOCK_PREFIX + ip + uri;
          String countKey = COUNT_PREFIX + ip + uri;
          Object o = redisTemplate.opsForValue().get(lockKey);
          if (Objects.nonNull(o)){
              log.info("用户{},访问{}接口,被禁用",ip,uri);
              //获取锁值不为空说明已经禁用,直接返回
              return true;
          }else {
              //未被禁用
              //获取访问次数
              Integer o1 = (Integer) redisTemplate.opsForValue().get(countKey);
              if (Objects.isNull(o1)){
                  log.info("用户{},访问{}接口,首次访问",ip,uri);
                  //首次访问,保存访问次数为1
                  redisTemplate.opsForValue().set(countKey,1,second,TimeUnit.SECONDS);
              }else {
                  //判断访问次数
                  if (o1 == times){
                      log.info("用户{},访问{}接口,达到次数限制被禁用",ip,uri);
                      //已经达到限制,禁用,返回
                      redisTemplate.opsForValue().set(lockKey,1,lockTime,TimeUnit.SECONDS);
                      //删除计数key,已经禁用,这个也就没必要了
                      redisTemplate.delete(countKey);
                      return true;
                  }else {
                      log.info("用户{},访问{}接口,现在第{}次访问",ip,uri,(o1 + 1));
                      //次数加1
                      //                    redisTemplate.opsForValue().set(countKey,++o1,second,TimeUnit.SECONDS);
                      Long increment = redisTemplate.opsForValue().increment(countKey);
                  }
              }
          }
          return false;
      }
      
    3. 到此限流方案完善

到了这里,关于SpringBoot定义拦截器+自定义注解+Redis实现接口防刷(限流)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 自定义注解与拦截器实现不规范sql拦截(自定义注解填充插件篇)

    在自定义注解与拦截器实现不规范sql拦截(拦截器实现篇)中提到过,写了一个idea插件来辅助对Mapper接口中的方法添加自定义注解,这边记录一下插件的实现。 在上一篇中,定义了一个自定义注解对需要经过where判断的Mapper sql方法进行修饰。那么,现在想使用一个idea插件来

    2024年01月23日
    浏览(38)
  • 防重复提交:自定义注解 + 拦截器(HandlerInterceptor)

    防重复提交:自定义注解 + 拦截器(HandlerInterceptor) 一、思路: 1、首先自定义注解; 2、创建拦截器实现类(自定义类名称),拦截器(HandlerInterceptor); 3、创建类:配置拦截器路径(拦截URL规则); 二、代码示例: 1、首先自定义注解; 2、创建拦截器实现类(自定义类名

    2024年02月10日
    浏览(29)
  • Springboot 自定义 Mybatis拦截器,实现 动态查询条件SQL自动组装拼接(玩具)

    ps:最近在参与3100保卫战,战况很激烈,刚刚打完仗,来更新一下之前写了一半的博客。 该篇针对日常写查询的时候,那些动态条件sql 做个简单的封装,自动生成(抛砖引玉,搞个小玩具,不喜勿喷)。 来看看我们平时写那些查询,基本上都要写的一些动态sql:   一个字段

    2024年02月12日
    浏览(34)
  • Springboot中自定义拦截器

    Spring Boot 中使用拦截器 参考:https://blog.csdn.net/taojin12/article/details/88342576?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170823498416800197050192%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257Drequest_id=170823498416800197050192biz_id=0utm_medium=distribute.pc_search_result.none-task-blog-2 all top_positive~defa

    2024年02月19日
    浏览(37)
  • SpringBoot自定义拦截器interceptor使用详解

    Spring Boot拦截器Intercepter详解 Intercepter是由Spring提供的Intercepter拦截器,主要应用在日志记录、权限校验等安全管理方便。 使用过程 1.创建自定义拦截器,实现HandlerInterceptor接口,并按照要求重写指定方法 HandlerInterceptor接口源码: 根据源码可看出HandlerInterceptor接口提供了三个

    2024年02月13日
    浏览(26)
  • SpringBoot加入拦截器——登录拦截器的实现

            拦截器 Interceptor 在 Spring MVC 中的地位等同于 Servlet 规范中的过滤器 Filter,拦截的是处理器的执行,由于是全局行为,因此常用于做一些通用的功能,如请求日志打印、权限控制等。         核心原理:AOP思想 preHandle:  预先处理,在目标的controller方法执行之前,进行

    2024年02月15日
    浏览(31)
  • 自定义拦截器实现

    在 Spring MVC 框架中, 拦截器作为一种机制, 用于对请求进行拦截. 拦截器可以在请求进入处理器之前、处理器返回处理之后、视图渲染之前等各个环节进行拦截. 拦截器通常用于实现一下功能 : 鉴权和身份认证 日志记录和统计 请求参数和校验和过滤 缓存和性能优化 路径重定向

    2024年02月09日
    浏览(37)
  • 一张思维导图带你打通SpringBoot自定义拦截器的思路

    🧑‍💻作者名称:DaenCode 🎤作者简介:啥技术都喜欢捣鼓捣鼓,喜欢分享技术、经验、生活。 😎人生感悟:尝尽人生百味,方知世间冷暖。 📖所属专栏:SpringBoot实战 在开发中,都离不开拦截器的使用。比如说在开发登录功能时,采用JWT登录时通过对token进行验证实现登

    2024年02月14日
    浏览(38)
  • Spring Boot入门(23):记录接口日志再也不难!用AOP和自定义注解给Spring Boot加上日志拦截器!

            在上两期中,我们着重介绍了如何集成使用 Logback 与 log4j2 日志框架的使用,今天我们讲解的主题依旧跟日志有关,不过不是使用何种开源框架,而是自己动手造。         Spring的核心之一AOP;AOP翻译过来叫面向切面编程, 核心就是这个切面. 切面表示从业务逻辑中

    2024年02月11日
    浏览(41)
  • springboot实现拦截器

    内容:继承 HandlerInterceptorAdapter 并实现 WebMvcConfigurer , 拦截器中的方法将preHandle-Controller-postHandle-affterCompletion的顺序执行。 注意:只有preHandle方法返回true时后面的方法才会执行。当拦截器链存在多个拦截器时,postHandle在所有拦截器内的所有拦截器返回成功时才会调用,而

    2024年02月02日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包