首页 > 数据库 >SpringBoot定义拦截器+自定义注解+Redis实现接口防刷(限流)

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

时间:2023-11-14 11:55:05浏览次数:40  
标签:拦截器 自定义 接口 times 访问 second 限流 注解 lockTime

  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中的所以接口都进行限流,如果某个接口上也添加了注解,那么就采用就近原则使用接口上注解的参数。仍然需要优化

  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. 到此限流方案完善

标签:拦截器,自定义,接口,times,访问,second,限流,注解,lockTime
From: https://www.cnblogs.com/ccx-lly/p/17830839.html

相关文章

  • Oracle sql自定义统计月范围
     思路: 1,使用SUBSTR(to_char(INSPECTION_DATE,'yyyy-mm-dd'),-2)取出天数, 2,使用case……when……then……判断取出的天数是否大于等于25号,如果是则将日期设置成下月第一天 如果小于等于24号,则设置成当月第一天 3,使用TRUNC(ADD_MONTHS(INSPECTION_DATE,1),'mm')增......
  • 自定义GPT已经出现,并将影响人工智能的一切,做好被挑战的准备了吗?
    原创|文BFT机器人OpenAI凭借最新突破:定制GPT站在创新的最前沿。预示着个性化数字协助的新时代到来,ChatGPT以前所未有的精度来满足个人需求和专业需求。自定义GPT具有变革性的特点——可以被定制为任何领域或任务的专家。我们可以想象一下为SEO研究量身定制的数字助理、能够筛选......
  • 微信小程序--自定义tabbar切换页面时,保留数据方案
    自定义的tabbar组件,每次切换页面时都会重新加载页面和数据,需要通过一些方法把tabbar菜单的数据保留下来,不要每次都请求数据。方案一:在app.js文件里定义全局数据(本次项目采用的是可以在后台管理里配置的数据,所以采用了方案一)1、在app.js文件里定义一个全局变量App({  globa......
  • 十三、自定义类型
    用户定义数据类型通俗定义:用户自己设计并实现的数据类型就称为用户自定义数据类型,即使这些数据类型基于系统数据类型。也可以理解为基础类型的一个延伸。用户定义数据类型三要素:1.数据类型的名称2.所基于的系统数据类型3.数据类型的可空性(是否可以为空)USE[Advanc......
  • 拦截器
    packagecom.comen.interceptor;importcom.comen.edata.bean.User;importcom.comen.edata.tools.JwtUtil;importorg.springframework.web.servlet.HandlerInterceptor;importorg.springframework.web.servlet.ModelAndView;importjavax.servlet.http.HttpServletR......
  • nuclei 快速&可自定义的基于DSL的漏洞扫描工具
    nuclei是基于golang开发的,可以使用基于yaml定义的dsl,支持扫描不少协议(tcp,dns,http,ssl,file,whois,websocket,headless,以及code)同时nuclei也提供了不少模版可以方便快速使用说明nuclei使用简单,主要包含两步,定义yaml文件,运行,同时提供了大量可用的模版是一个很不错的安全工具,很值......
  • Android自定义View使用系统自有属性
    原文链接:Android自定义View使用系统自有属性-Stars-One的杂货小窝本篇默认各位有自定义View的相关知识,本篇只作为一个小知识点补充有这样一种情况,比如说我们的一个自定义View中有个maxLines的属性,但是我们会注意到这个maxLines其实Android里面已经存在了(如TextView中),我们能......
  • SharePoint 页面中插入自定义代码
    我们都知道SharePoint是对页面进行编辑的。对于一些有编程基础的人来说,可能需要对页面中插入代码,这样才能更好的对页面进行配置。但是在新版本的SharePointmodern页面来说,虽然我们可以插入Embed组件。但是Embed组件中是不允许提供Script和Html脚本的。只能插入iFrame......
  • SharePoint 页面中插入自定义代码
    我们都知道SharePoint是对页面进行编辑的。对于一些有编程基础的人来说,可能需要对页面中插入代码,这样才能更好的对页面进行配置。但是在新版本的SharePointmodern页面来说,虽然我们可以插入Embed组件。但是Embed组件中是不允许提供Script和Html脚本的。只能插入iF......
  • vue自定义指令
    app.vue<template><divclass=""><!--自定义指令全局<inputv-focustype="text"name=""id=""><br>自定义指令局部<inputv-focus2type="text"name=""id="&......