1. 程式人生 > 程式設計 >Spring boot通過AOP防止API重複請求程式碼例項

Spring boot通過AOP防止API重複請求程式碼例項

這篇文章主要介紹了Spring boot通過AOP防止API重複請求程式碼例項,文中通過示例程式碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下

實現思路

基於Spring Boot 2.x

自定義註解,用來標記是哪些API是需要監控是否重複請求

通過Spring AOP來切入到Controller層,進行監控

檢驗重複請求的Key:Token + ServletPath + SHA1RequestParas

  • Token:使用者登入時,生成的Token
  • ServletPath:請求的Path
  • SHA1RequestParas:將請求引數使用SHA-1雜湊演算法加密

使用以上三個引數拼接的Key作為去判斷是否重複請求

由於專案是基於叢集的,使用Redis儲存Key,而且redis的特性,key可以設定在規定時間內自動刪除。這裡的這個規定時間,就是api在規定時間內不能重複提交。

自定義註解(註解作用於Controller層的API)

 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface NoRepeatSubmission {
 
 }

切面邏輯

import com.gotrade.apirepeatrequest.annotation.NoRepeatSubmission;
import com.gotrade.apirepeatrequest.common.JacksonSerializer;
import com.gotrade.apirepeatrequest.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;



@Slf4j
@Aspect
@Component
public class NoRepeatSubmissionAspect {

  @Autowired
  RedisTemplate<String,String> redisTemplate;

  /**
   * 環繞通知
   * @param pjp
   * @param ars
   * @return
   */
  @Around("execution(public * com.gotrade.apirepeatrequest.controller..*.*(..)) && @annotation(ars)")
  public Object doAround(ProceedingJoinPoint pjp,NoRepeatSubmission ars) {
    ValueOperations<String,String> opsForValue = redisTemplate.opsForValue();
    try {
      if (ars == null) {
        return pjp.proceed();
      }

      HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

      String token = request.getHeader("Token");
      if (!checkToken(token)) {
        return Result.failure("Token無效");
      }
      String servletPath = request.getServletPath();
      String jsonString = this.getRequestParasJSONString(pjp);
      String sha1 = this.generateSHA1(jsonString);

      // key = token + servlet path
      String key = token + "-" + servletPath + "-" + sha1;

      log.info("\n{\n\tServlet Path: {}\n\tToken: {}\n\tJson String: {}\n\tSHA-1: {}\n\tResult Key: {} \n}",servletPath,token,jsonString,sha1,key);

      // 如果Redis中有這個key,則url視為重複請求
      if (opsForValue.get(key) == null) {
        Object o = pjp.proceed();
        opsForValue.set(key,String.valueOf(0),3,TimeUnit.SECONDS);
        return o;
      } else {
        return Result.failure("請勿重複請求");
      }
    } catch (Throwable e) {
      e.printStackTrace();
      return Result.failure("驗證重複請求時出現未知異常");
    }
  }

  /**
   * 獲取請求引數
   * @param pjp
   * @return
   */
  private String getRequestParasJSONString(ProceedingJoinPoint pjp) {
    String[] parameterNames = ((MethodSignature) pjp.getSignature()).getParameterNames();
    ConcurrentHashMap<String,String> args = null;

    if (Objects.nonNull(parameterNames)) {
      args = new ConcurrentHashMap<>(parameterNames.length);
      for (int i = 0; i < parameterNames.length; i++) {
        String value = pjp.getArgs()[i] != null ? pjp.getArgs()[i].toString() : "null";
        args.put(parameterNames[i],value);
      }
    }
    return JacksonSerializer.toJSONString(args);
  }

  private boolean checkToken(String token) {
    if (token == null || token.isEmpty()) {
      return false;
    }
    return true;
  }

  private String generateSHA1(String str){
    if (null == str || 0 == str.length()){
      return null;
    }
    char[] hexDigits = { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
    try {
      MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
      mdTemp.update(str.getBytes(StandardCharsets.UTF_8));

      byte[] md = mdTemp.digest();
      int j = md.length;
      char[] buf = new char[j * 2];
      int k = 0;
      for (int i = 0; i < j; i++) {
        byte byte0 = md[i];
        buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
        buf[k++] = hexDigits[byte0 & 0xf];
      }
      return new String(buf);
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    return null;
  }
}

切面主要邏輯程式碼,就是獲取request中相關的資訊,然後再拼接成一個key;判斷在redis是否存在,不存在就新增並設定規定時間後自動移除,存在就是重複請求 。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。