恳求重复提交的损害

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

防恳求重复提交的方案

前端

  • 在用户第一次点击按钮后,即禁用提交按钮。
  • 约束用户提交恳求距离,在一定的时刻距离内只允许用户发起某个恳求一次。
  • 在表单提交前,查看前一次恳求是否提交成功,已成功的话则提示用户无需再重复提交。

后端

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

这种防恳求重复提交的完成有根据Filter的完成,也有根据HandlerInterceptor的完成。最终考量下笔者认为运用RequestBodyAdviceAdapter类来完成代码完成愈加简练,装备愈加简单。

在此笔者提供一个注解+RequestBodyAdviceAdapter合作运用的防重复提交的完成。
可是这个方案有个小坏处。仅收效于有RequestBody注解的参数,因为运用RequestBodyAdvice来完成。可是大部分咱们需要做恳求防重复提交的接口一般都是POST恳求,且有requestBody。

完好完成在开源项目中:github.com/valarchie/A…

完成

声明注解

/**
 * 自定义注解防止表单重复提交
 * 仅收效于有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