实现思路
基于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)
1 @Target(ElementType.METHOD) 2 @Retention(RetentionPolicy.RUNTIME) 3 public @interface NoRepeatSubmission { 4 5 }
切面逻辑
1 import com.gotrade.apirepeatrequest.annotation.NoRepeatSubmission; 2 import com.gotrade.apirepeatrequest.common.JacksonSerializer; 3 import com.gotrade.apirepeatrequest.model.Result; 4 import lombok.extern.slf4j.Slf4j; 5 import org.aspectj.lang.ProceedingJoinPoint; 6 import org.aspectj.lang.annotation.Around; 7 import org.aspectj.lang.annotation.Aspect; 8 import org.aspectj.lang.reflect.MethodSignature; 9 import org.springframework.beans.factory.annotation.Autowired; 10 import org.springframework.data.redis.core.RedisTemplate; 11 import org.springframework.data.redis.core.ValueOperations; 12 import org.springframework.stereotype.Component; 13 import org.springframework.web.context.request.RequestContextHolder; 14 import org.springframework.web.context.request.ServletRequestAttributes; 15 16 import javax.servlet.http.HttpServletRequest; 17 import java.nio.charset.StandardCharsets; 18 import java.security.MessageDigest; 19 import java.security.NoSuchAlgorithmException; 20 import java.util.Objects; 21 import java.util.concurrent.ConcurrentHashMap; 22 import java.util.concurrent.TimeUnit; 23 24 /** 25 * @author jason.tang 26 * @create 2019/4/8 27 * @description 28 */ 29 30 @Slf4j 31 @Aspect 32 @Component 33 public class NoRepeatSubmissionAspect { 34 35 @Autowired 36 RedisTemplate<String, String> redisTemplate; 37 38 /** 39 * 环绕通知 40 * @param pjp 41 * @param ars 42 * @return 43 */ 44 @Around("execution(public * com.gotrade.apirepeatrequest.controller..*.*(..)) && @annotation(ars)") 45 public Object doAround(ProceedingJoinPoint pjp, NoRepeatSubmission ars) { 46 ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); 47 try { 48 if (ars == null) { 49 return pjp.proceed(); 50 } 51 52 HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); 53 54 String token = request.getHeader("Token"); 55 if (!checkToken(token)) { 56 return Result.failure("Token无效"); 57 } 58 String servletPath = request.getServletPath(); 59 String jsonString = this.getRequestParasJSONString(pjp); 60 String sha1 = this.generateSHA1(jsonString); 61 62 // key = token + servlet path 63 String key = token + "-" + servletPath + "-" + sha1; 64 65 log.info("\n{\n\tServlet Path: {}\n\tToken: {}\n\tJson String: {}\n\tSHA-1: {}\n\tResult Key: {} \n}", servletPath, token, jsonString, sha1, key); 66 67 // 如果Redis中有这个key, 则url视为重复请求 68 if (opsForValue.get(key) == null) { 69 Object o = pjp.proceed(); 70 opsForValue.set(key, String.valueOf(0), 3, TimeUnit.SECONDS); 71 return o; 72 } else { 73 return Result.failure("请勿重复请求"); 74 } 75 } catch (Throwable e) { 76 e.printStackTrace(); 77 return Result.failure("验证重复请求时出现未知异常"); 78 } 79 } 80 81 /** 82 * 获取请求参数 83 * @param pjp 84 * @return 85 */ 86 private String getRequestParasJSONString(ProceedingJoinPoint pjp) { 87 String[] parameterNames = ((MethodSignature) pjp.getSignature()).getParameterNames(); 88 ConcurrentHashMap<String, String> args = null; 89 90 if (Objects.nonNull(parameterNames)) { 91 args = new ConcurrentHashMap<>(parameterNames.length); 92 for (int i = 0; i < parameterNames.length; i++) { 93 String value = pjp.getArgs()[i] != null ? pjp.getArgs()[i].toString() : "null"; 94 args.put(parameterNames[i], value); 95 } 96 } 97 return JacksonSerializer.toJSONString(args); 98 } 99 100 private boolean checkToken(String token) { 101 if (token == null || token.isEmpty()) { 102 return false; 103 } 104 return true; 105 } 106 107 private String generateSHA1(String str){ 108 if (null == str || 0 == str.length()){ 109 return null; 110 } 111 char[] hexDigits = { ‘0‘, ‘1‘, ‘2‘, ‘3‘, ‘4‘, ‘5‘, ‘6‘, ‘7‘, ‘8‘, ‘9‘, 112 ‘a‘, ‘b‘, ‘c‘, ‘d‘, ‘e‘, ‘f‘}; 113 try { 114 MessageDigest mdTemp = MessageDigest.getInstance("SHA1"); 115 mdTemp.update(str.getBytes(StandardCharsets.UTF_8)); 116 117 byte[] md = mdTemp.digest(); 118 int j = md.length; 119 char[] buf = new char[j * 2]; 120 int k = 0; 121 for (int i = 0; i < j; i++) { 122 byte byte0 = md[i]; 123 buf[k++] = hexDigits[byte0 >>> 4 & 0xf]; 124 buf[k++] = hexDigits[byte0 & 0xf]; 125 } 126 return new String(buf); 127 } catch (NoSuchAlgorithmException e) { 128 e.printStackTrace(); 129 } 130 return null; 131 } 132 }
切面主要逻辑代码,就是获取request中相关的信息,然后再拼接成一个key;判断在redis是否存在,不存在就添加并设置规定时间后自动移除,存在就是重复请求 。
原文地址:https://www.cnblogs.com/21-Gram/p/11971340.html
时间: 2024-10-28 15:27:38