背景
先了解一下需求详情:在项目的开发中,安全这块是必不可少的,今天的问题是:防止API重复请求。为什么要防止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在规定时间内不能重复提交。以上就是个人的实现思路,下面一步一步来剖析。
剖析
- 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmission {
}
- 注解作用于Controller层的API(数据都是模拟数据)
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping({"", "/"})
public Result<String> getUser() {
return Result.success("Jason");
}
@NoRepeatSubmission
@GetMapping("/{userNum}")
public Result<String> getUser(@PathVariable Integer userNum) {
String str = "";
if (userNum == 1) {
str = "Jason";
} else if (userNum == 2) {
str = "Rose";
} else {
str = "Other";
}
return Result.success(str);
}
@NoRepeatSubmission
@PostMapping({"", "/{companyID}"})
public Result<User> addUser(@PathVariable String companyID, @RequestBody User user) {
return Result.success(user);
}
}
- 切面逻辑(重点)
@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);
}
}
切面贴出主要逻辑代码,就是获取request中相关的信息,然后再拼接成一个key;判断在redis是否存在,不存在就添加并设置规定时间后自动移除,存在就是重复请求 。
代码总结与拓展
- 该功能是使用Spring AOP与Redis的特性进行开发
- 有关详细代码并未贴出,防止篇幅太长,源码请看这里
- 这里只是规定时间内防止重复提交,如果规定时间内,根据请求的次数去限制,可以设置key对应的value,用value进行判断
- 下篇文章将给出使用Docker部署集群环境测试。
网友评论