实现思路
基于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 { }
切面逻辑
@Slf4j @Aspect @Component public class NoRepeatSubmissionAspect { @Autowired RedisTemplate<String, String> redisTemplate; /** * 环绕通知 * @param pjp * @param ars * @return */ @Around("execution(public * com.example.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); } }
切面主要逻辑代码,就是获取request中相关的信息,然后再拼接成一个key;判断在redis是否存在,不存在就添加并设置规定时间后自动移除,存在就是重复请求 。