天天看點

中小型項目統一處理請求重複送出

作者:Java架構學習指南

請求重複送出的危害

  • 資料重複:例如使用者重複送出表單,造成資料重複。
  • 資源浪費:多次重複請求送出将會浪費伺服器的處理資源。但這個相比資料重複的危害性較小。
  • 不一緻性:假設我們觸發請求增加使用者的積分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();
}           

這是筆者關于中小型項目防請求重複送出的實作,如有不足歡迎大家評論指正。

繼續閱讀