服务器接口安全校验限流处理方案(java版)

         1.背景
         2.实现思路
         3.实现过程

1.背景

     现在运行项目中,需要单独给下游服务商提供数据访问接口以完成项目合作,需要进行以下安全适配处理:

1.授权校验:对下游服务商进行区别与现在已有的接口授权信息校验;即现在项目需要提供单独的授权校验处理;
2.ip限制:仅添加白名单的服务商ip可以访问;
3.访问次数限制:不同的接口实现不同的QPS(每秒查询的次数)访问.

     现将需求实现过程进行记录,希望对于有同样需求的同学有所帮助!

2.实现思路

    使用aop实现方法拦截,切面处添加校验业务处理;
    1.授权校验:
        提供服务商授权信息获取接口,服务商访问接口均须带有授权信息方可访问(header中携带认证信息);
    2.ip限制:
        获取每次请求中的ip地址,与数据库的记录的白名单ip进行比对,没有记录的视为非法ip访问;
    3.访问次数限制:
    采用注解方式动态指定不同接口指定不同的访问频次,轻量级java缓存方案ExpiringMap实现访问次数是否达到设置上限校验;

3.实现过程

    现有项目原有授权验证逻辑与新增服务商访问授权逻辑简要说明:
    现在项目接口均进行shiro校验,保持原有逻辑不变,对新增的服务商接口放开校验限制,认证授权采用新的校验规则(自定义aop实现).具体代码实现如下:
    自定义aop:IntelligenceAop.java:

@Aspect
@Component
@Slf4j
public class IntelligenceAop {
    
    



    @Autowired
    private RedisTemplate redisTemplate;

    // 接口访问次数限制缓存,格式:{"接口名":{"访问ip",访问次数}}
    private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> visitCountMap = new ConcurrentHashMap<>();

	// 切入点表达式指定服务商访问接口范围
    @Pointcut("execution(* com.A.api.intelligence.controller.*.*(..))")
    public void log() {
    
    }

	// 对getToken获取授权信息不进行安全校验
    @Pointcut("execution(* com.A.api.intelligenceCode.controller.IntelligenceController.getToken(..))")
    public void excludeLog() {
    
    }

	// 最终切入点表达式范围为排除getToken之外的所有服务商接口
    @Pointcut("log() && !excludeLog()")
    public void allPointcutWeb() {
    
    
    }

 	// doAround优先于before执行,三项安全校验从这里处理
    @Around("allPointcutWeb()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
    
    

        // 校验ip是否添加访问白名单
        HttpServletRequest request = HttpServletRequestUtil.getRequest();
        String thirdToken = request.getHeader("thirdToken");
        if(StrUtil.isBlank(thirdToken)){
    
    
            log.error("非法访问ip:{}",request.getRemoteAddr());
            throw new BusinessException("非法访问!");
        }

        // 校验三方请求token是否有效
        checkThirdToken(thirdToken);

        // 校验接口访问次数
        checkVisitCount(pjp, request);

        Object ob = pjp.proceed();// ob 为方法的返回值
       
        return ob;
    }

   // 校验三方请求token是否有效
    private void checkThirdToken(String thirdToken) {
    
    
        String thirdTokenRedis = (String)redisTemplate.opsForValue().get("thirdToken");
        if(StrUtil.isBlank(thirdTokenRedis)){
    
    
            throw new BusinessException("认证授权信息已过期,请重新获取!");
        }
        if(!StrUtil.equals(thirdToken,thirdTokenRedis)){
    
    
            log.error("非法token信息访问,请求token:{},缓存token:{},",thirdToken,thirdTokenRedis);
            throw new BusinessException("非法访问");
        }
    }

    // 校验访问次数
    private void checkVisitCount(ProceedingJoinPoint pjp, HttpServletRequest request) throws Exception {
    
    
        VisitCountAnnotation visitCountAnnotation = getVisitCountAnnotation(pjp);
        if(ObjectUtil.isNotNull(visitCountAnnotation)){
    
    
            // 第一个参数是访问路径, 第二个参数是默认值
            ExpiringMap<String, Integer> map = visitCountMap.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
            // 接口访问次数
            Integer visitCount = map.getOrDefault(request.getRemoteAddr(), 0);


            if (visitCount >= visitCountAnnotation.count()) {
    
     // 超过次数,不执行目标方法
               throw new BusinessException("非法访问:已超过最大访问次数限制,请稍后重试!");
            } else if (visitCount == 0){
    
     // 第一次请求时,设置开始有效时间
                map.put(request.getRemoteAddr(), visitCount + 1, ExpirationPolicy.CREATED, visitCountAnnotation.time(), TimeUnit.MILLISECONDS);
            } else {
    
     // 未超过次数, 记录数据加一
                map.put(request.getRemoteAddr(), visitCount + 1);
            }
            visitCountMap.put(request.getRequestURI(), map);
        }
    }

   

  // 判断是否存在VisitCountAnnotation注解
    private VisitCountAnnotation getVisitCountAnnotation(JoinPoint joinPoint) throws Exception
    {
    
    
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null)
        {
    
    
            return method.getAnnotation(VisitCountAnnotation.class);
        }
        return null;
    }
}

    访问次数限制注解VisitCountAnnotation:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitCountAnnotation {
    
    

    // QPS:2000
    long time() default 1000; // 限制时间 单位:毫秒

    int count() default 2000; // 允许请求的次数

}

    控制类IntelligenceController.java :

@RequestMapping("/A")
@Validated
@RestController
public class IntelligenceController {
    
    

    @Autowired
    private IntelligenceServiceImpl intelligenceService;


    @GetMapping("/getToken")
    @ApiOperation(value = "获取请求认证信息")
    public ApiResult getToken() throws Exception {
    
    
        String thirdToken = intelligenceService.getToken();
        return ApiResult.ok(thirdToken);
    }

    @VisitCountAnnotation()  // 如有需求也可以指定访问次数与访问时间,否则按照默认处理
    @GetMapping("/findUserInfoList")
    @ApiOperation(value = "查询用户信息")
    public ApiResult<PageInfo<IntelligenceCodeUserInfo>> findUserInfoList(@NotNull(message = "当前页面码数不允许为空!")
                                                                                                  @Min(value = 1,message = "当前页面码数不允许为0!") Integer currentPage,
                                                                                      @NotNull(message = "每页显示条数不允许为空!")
                                                                                      @Min(value = 1,message = "每页显示条数不允许为0!") Integer pageSize) throws Exception {
    
    
        PageInfo<IntelligenceCodeUserInfo> intelligenceCodeUserInfoPageInfo = intelligenceCodeService.findUserInfoList(currentPage,pageSize);
        return ApiResult.ok(intelligenceCodeUserInfoPageInfo);
    }
}

    实现类IntelligenceServiceImpl.java:

@Service
@Slf4j
public class IntelligenceServiceImpl implements IntelligenceService {
    
    

    @Autowired
    private IntelligenceMapper intelligenceMapper;

    @Autowired
    private RedisTemplate redisTemplate;



    // 三方token缓存时间,单位秒,默认2小时,读取配置文件信息
    @Value("${intelligence.expireTime}")
    @Autowired
    private Integer expireTime;


  // 查询智能码会员信息
    @Override
    public PageInfo<IntelligenceCodeUserInfo> findUserInfoList(Integer currentPage,Integer pageSize) {
    
    
        PageHelper.startPage(currentPage,pageSize);
        List<IntelligenceCodeUserInfo> intelligenceUserInfoList = intelligenceCodeMapper.findUserInfoList();
        PageInfo<IntelligenceCodeUserInfo> intelligenceCodeUserInfoPageInfo = new PageInfo<>(intelligenceUserInfoList);
        return intelligenceCodeUserInfoPageInfo;
    }

  // 获取三方认证授权信息
    @Override
    public synchronized String getToken() throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    
    


        // 获取当前访问ip
        HttpServletRequest request = HttpServletRequestUtil.getRequest();
        String visitIp = request.getRemoteAddr();

        // 判断当前访问ip是否添加白名单,读取数据库添加的ip白名单
        String whiteInfo = intelligenceCodeMapper.findWhiteIp();
        if(StrUtil.isBlank(whiteInfo)){
    
    
            throw new BusinessException("数据异常:获取为空!");
        }

        JSONObject jsonObject = JSONUtil.parseObj(whiteInfo);
        List whiteIps = jsonObject.get("whiteIp", List.class);
        if(!CollectionUtil.contains(whiteIps,visitIp)){
    
    
            throw new BusinessException("非法访问:未添加ip白名单!");
        }

        // 查询是否存在认证缓存信息,如果存在则直接返回,不存在则重新生成
        String redisThirdToken = (String) redisTemplate.opsForValue().get("thirdToken");
        if(StrUtil.isNotBlank(redisThirdToken)){
    
    
            return redisThirdToken;
        }

        // 生成访问token信息,可自定义token生成规则,此处不再展开
        String thirdToken = getThirdToken();
        
        // 缓存中设置thirdToken
        redisTemplate.opsForValue().set("thirdToken",thirdToken, expireTime, TimeUnit.SECONDS);


        return thirdToken;
    }

}

    以上是服务端处理安全校验的思路分析以及实现过程,如果感觉有所帮助欢迎点赞收藏或是评论区留言!

猜你喜欢

转载自blog.csdn.net/weixin_43401380/article/details/129029303