防止API重复请求(集群环境)

Java基础

浏览数:189

2019-11-2

背景

先了解一下需求详情:在项目的开发中,安全这块是必不可少的,今天的问题是:防止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部署集群环境测试。

Reference

https://www.jianshu.com/p/09860b74658e

作者:JasonDev