中小型项目统一处理请求重复提交

Posted valarchie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了中小型项目统一处理请求重复提交相关的知识,希望对你有一定的参考价值。

请求重复提交的危害

  • 数据重复:例如用户重复提交表单,造成数据重复。
  • 资源浪费:多次重复请求提交将会浪费服务器的处理资源。但这个相比数据重复的危害性较小。
  • 不一致性:假设我们触发请求增加用户的积分500,如果多次触发这个请求,积分是累加的。这个危害性比重复的数据更大。
  • 安全性:例如我们在登录页面触发手机验证码的发送请求。频繁触发这个请求将会耗费我们的验证码成本。

防请求重复提交的方案

前端

  • 在用户第一次点击按钮后,即禁用提交按钮。
  • 限制用户提交请求间隔,在一定的时间间隔内只允许用户发起某个请求一次。
  • 在表单提交前,检查前一次请求是否提交成功,已成功的话则提示用户无需再重复提交。

后端

  • 严谨的做法
    • Token机制,在每一个请求中都添加一个Token。Token由服务端生成并发放给前端。服务端接收到请求时,根据Token进行校验。看这个Token是否已被使用。(一般基于缓存)
    • 唯一标志,比如在创建订单的时候,即生成一个唯一的订单号,并将其作为订单的唯一标识。在后续的请求中携带该订单号。当收到订单创建请求时,检查订单号是否已经存在。(一般基于数据库)
  • 非严谨的做法
    • 后端拦截请求,检查请求的用户和参数是否和上次请求相同,相同的话即为重复请求。

这种防请求重复提交的实现有基于Filter的实现,也有基于HandlerInterceptor的实现。最后考量下笔者认为利用RequestBodyAdviceAdapter类来实现代码实现更加简洁,配置更加简单。

在此笔者提供一个注解+RequestBodyAdviceAdapter配合使用的防重复提交的实现。
但是这个方案有个小弊端。仅生效于有RequestBody注解的参数,因为使用RequestBodyAdvice来实现。但是大部分我们需要做请求防重复提交的接口一般都是POST请求,且有requestBody。

完整实现在开源项目中:https://github.com/valarchie/AgileBoot-Back-End

实现

声明注解

/**
 * 自定义注解防止表单重复提交
 * 仅生效于有RequestBody注解的参数  因为使用RequestBodyAdvice来实现
 * @author valarchie
 */
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit 

    /**
     * 间隔时间(s),小于此时间视为重复提交
     */
    int interval() default 5;


继承RequestBodyAdviceAdapter实现ResubmitInterceptor

大致的实现是。

  • 覆写了supports方法,指明我们仅处理拥有Resubmit注解的方法。
  • 生成每一个请求的签名作为Key。key的生成由generateResubmitRedisKey方法实现。格式如下:resubmit:::。比如用户是userA。我们请求的类是UserService。方法名是addUser。则这个key为resubmit:userA:UserService:addUser
  • 将Key和请求的参数作为值存到redis当中去
  • 每一次请求过来时,我们检查缓存中这个请求的签名对应的参数是否相同,相同的话即为重复请求。
/**
 * 重复提交拦截器 如果涉及前后端加解密的话  也可以通过继承RequestBodyAdvice来实现
 *
 * @author valarchie
 */
@ControllerAdvice(basePackages = "com.agileboot")
@Slf4j
@RequiredArgsConstructor
public class ResubmitInterceptor extends RequestBodyAdviceAdapter 

    public static final String NO_LOGIN = "Anonymous";
    public static final String RESUBMIT_REDIS_KEY = "resubmit:::";

    @NonNull
    private RedisUtil redisUtil;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) 
        return methodParameter.hasMethodAnnotation(Resubmit.class);
    

    /**
     * @param body 仅获取有RequestBody注解的参数
     */
    @NotNull
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) 
        // 仅获取有RequestBody注解的参数
        String currentRequest = JSONUtil.toJsonStr(body);

        Resubmit resubmitAnno = parameter.getMethodAnnotation(Resubmit.class);
        if (resubmitAnno != null) 
            String redisKey = generateResubmitRedisKey(parameter.getMethod());

            log.info("请求重复提交拦截,当前key:, 当前参数:", redisKey, currentRequest);

            String preRequest = redisUtil.getCacheObject(redisKey);
            if (preRequest != null) 
                boolean isSameRequest = Objects.equals(currentRequest, preRequest);

                if (isSameRequest) 
                    throw new ApiException(ErrorCode.Client.COMMON_REQUEST_RESUBMIT);
                
            
            redisUtil.setCacheObject(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);
        

        return body;
    

    public String generateResubmitRedisKey(Method method) 
        String username;

        try 
            LoginUser loginUser = AuthenticationUtils.getLoginUser();
            username = loginUser.getUsername();
         catch (Exception e) 
            username = NO_LOGIN;
        

        return StrUtil.format(RESUBMIT_REDIS_KEY,
            method.getDeclaringClass().getName(),
            method.getName(),
            username);
    

使用

通过在Controller上打上Resubmit注解即可,interval即多久的间隔内相同参数视为重复请求。

/**
 * 新增通知公告
 */
@Resubmit(interval = 60)
@PostMapping
public ResponseDTO<Void> add(@RequestBody NoticeAddCommand addCommand) 
    noticeApplicationService.addNotice(addCommand);
    return ResponseDTO.ok();

这是笔者关于中小型项目防请求重复提交的实现,如有不足欢迎大家评论指正。

全栈技术交流群:1398880

防止重复数据

在实际项目开发中,提交表单时常常由于网络或者其原因,用户点击提交按钮误认为自己没有操作成功,进而会重复提交按钮操作次数,如果页面前端代码没有做一些相应的处理,通常会导致多条同样的数据插入数据库,导致脏数据的增加。要避免这种现象,在$.ajax请求中的beforeSend方法中把提交按钮禁用掉,等到Ajax请求执行完毕,在恢复按钮的可用状态。

举个例子:

// 提交表单数据到后台处理
$.ajax({
  type: "post",
  data: studentInfo,
  contentType: "application/json",
  url: "/Home/Submit",
  beforeSend: function () { // 禁用按钮防止重复提交
    $("#submit").attr({
      disabled: "disabled"
    });
  },
  success: function (data) {
    if (data == "Success") {//清空输入框
      clearBox();
    }
  },
  complete: function () {
    $("#submit").removeAttr("disabled");
  },
  error: function (data) {
    console.info("error: " + data.responseText);
  }
});

以上是关于中小型项目统一处理请求重复提交的主要内容,如果未能解决你的问题,请参考以下文章

Vue项目按钮重复提交

$.ajax防止多次点击重复提交的方法

解决Vue项目按钮重复(高频)提交问题!

防止重复数据

Spring中的统一异常处理

Springboot 使用AOP实现防止接口重复提交