美文网首页
防止API重复请求(集群环境)

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

作者: Java酸不酸 | 来源:发表于2019-04-08 16:30 被阅读0次

    背景

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

    相关文章

      网友评论

          本文标题:防止API重复请求(集群环境)

          本文链接:https://www.haomeiwen.com/subject/wpauiqtx.html