• 防止请求重复提交:注解+拦截器的实现方案


    了解请求重复提交

    请求重复提交是指用户在一次请求还未处理完成时,重复提交了相同的请求。这种情况通常发生在网络延迟、用户误操作或系统性能不佳等情况下。

    请求重复提交可能会导致以下问题和影响:

    1. 数据不一致:如果重复提交的请求包含了对数据的修改操作,那么可能会导致数据不一致的问题,例如重复购买商品、重复支付订单等。

    2. 系统资源浪费:重复提交的请求会占用系统资源,导致服务器负载过高、响应时间变慢等问题,影响系统性能和用户体验。

    3. 安全问题:某些敏感操作(如支付、修改密码等)如果被重复提交,可能会导致安全问题,例如重复支付导致资金损失、重复修改密码导致账号被盗等。

    因此,防止请求重复提交是保证系统数据一致性、提高系统性能和保障系统安全的重要措施。

    解决思路

    在 Spring Boot 中有效地防止用户在短时间内重复提交表单或请求,我们可以结合 Redis 来实现这个功能,我们可以将用户的请求信息存储在缓存中,这样我们就可以实时跟踪用户请求的状态,同时也可以提高系统的性能。为了限制用户在短时间内重复提交相同的请求,我们可以设置一个时间间隔来限制重复提交。用户在指定时间间隔内提交相同的请求时,将返回一个错误信息,否则请求将被正常处理。因此,通过这个方法,我们可以有效地防止用户在短时间内提交重复的请求,避免系统资源的浪费。通常情况下,我们可以使用注解+拦截器的方式来实现这个功能。

    注解是一种在代码中添加元数据的方式,它可以为方法、类、字段等添加额外的信息。在请求重复提交的场景中,我们可以通过在方法或接口上添加自定义注解,在运行时通过反射机制来获取并解析注解信息,从而执行相应的逻辑。

    而拦截器则是一种在请求到达控制器之前对请求进行处理的组件。它可以拦截请求,并在请求到达目标处理程序之前或之后执行一些预处理或后处理操作。

    具体工作流程图如下:

    image-20231028152416711

    具体实现

    1. 首先我们需要创建一个自定义注解,用于标记需要进行重复请求拦截的方法。例如,我们在项目中创建一个名为@RepeatSubmit的注解:

      @Inherited
      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface RepeatSubmit {
      
          /**
           * 间隔时间(ms),小于此时间视为重复提交
           */
          public int interval() default 60000;
      
          /**
           * 提示消息
           */
          public String message() default "不允许重复提交,请稍候再试";
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    2. 创建一个拦截器类,用于拦截带有@RepeatSubmit注解的请求。在拦截器中,可以通过保存请求的唯一标识(如token)来判断请求是否已经被处理过。

      @Component
      public class RepeatSubmitInterceptor implements HandlerInterceptor {
      
          @Autowired
          private RedisCache redisCache;
      
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      
              // 判断处理器对象是否为HandlerMethod类型
              if (handler instanceof HandlerMethod) {
                  HandlerMethod handlerMethod = (HandlerMethod) handler;
                  Method method = handlerMethod.getMethod();
      
                  // 获取方法上的RepeatSubmit注解
                  RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
      
                  if (annotation != null) {
                      // 获取请求的IP地址、URI、请求方法、请求参数和请求体
                      String ipAddr = IpUtils.getIpAddr(request);
                      String requestURI = request.getRequestURI();
                      String requestMethod = request.getMethod();
                      Map<String, String> requestParams = ServletUtils.getRequestParams(request);
                      Map<String, String> requestBody = ServletUtils.getRequestBody(request);
      
                      // 创建缓存数据对象,并将请求信息存入其中
                      Map<String,Object> cacheData = new HashMap<>();
                      cacheData.put("ip",ipAddr);
                      cacheData.put("uri",requestURI);
                      cacheData.put("method",requestMethod);
                      cacheData.put("params",requestParams);
                      cacheData.put("body",requestBody);
      
                      // 将缓存数据进行MD5加密
                      String md5V = MD5Utils.encrypt(cacheData);
      
                      // 在Redis缓存中设置缓存对象,如果已存在则返回false
                      Boolean b = redisCache.setCacheObjectIfAbsent("repeat_submit:"+ipAddr+":"+md5V, cacheData, annotation.interval(), TimeUnit.MILLISECONDS);
      
                      if(b){
                          // 如果缓存设置成功,则放行请求
                          return true;
                      }else {
                          // 如果缓存已存在,则返回重复提交错误信息
                          AjaxResult ajaxResult = AjaxResult.error(annotation.message());
                          ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
                          return false;
                      }
                  }
      
                  // 如果方法没有RepeatSubmit注解,则放行请求
                  return true;
              }else {
                  // 如果处理器对象不是HandlerMethod类型,则放行请求
                  return true;
              }
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
    3. 在Spring Boot的配置类中注册拦截器:

      @Configuration
      public class WebMvcConfig implements WebMvcConfigurer {
      
          @Autowired
          private RepeatSubmitInterceptor repeatSubmitInterceptor;
      
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry
                  .addInterceptor(repeatSubmitInterceptor)
                  .addPathPatterns("/**");
          }
          
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    4. 在需要进行重复请求拦截的方法上添加@DuplicateSubmitToken注解:

      @Controller
      public class MyController {
      
          @RequestMapping("/submit")
          @RepeatSubmit
          public String submit() {
              // 处理请求逻辑
              return "success";
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    5. 启动项目,使用 ApiFox 发起请求访问/submit接口,发送成功:

      image-20231028144719930

    6. 请求发送成功后,查看Redis中的缓存数据,包含我们请求时接口数据:

      image-20231028144758402

    7. 再次发起同样的请求,直接返回提示信息“不允许重复提交,请稍候再试”:

      image-20231028144902473

  • 相关阅读:
    商标申请注册交费就一定会下注册证?
    知识图谱从入门到应用——知识图谱的应用
    前端sql条件拼接js工具
    HTML5 新增的input 表单属性
    leetcode做题笔记216. 组合总和 III
    C语言刷题(一)
    FFmpeg进阶:视频滤镜大全
    SQL语句中对时间字段进行区间查询
    element-china-area-data插件vue3做省市区的下拉选择,用3个独立的el-select实现
    线程与多线程(二)
  • 原文地址:https://blog.csdn.net/qq_20185737/article/details/134092378