如何活用自定义注解和AOP实现限制ip访问

71 阅读3分钟

1.问题分析

在某些场景下,需要对接口的访问进行限制,以防止恶意攻击或过度频繁的请求对系统造成压力。通过控制 IP 访问次数,可以有效地保护系统的稳定性和安全性。

2.解决方案

第一步在 yaml 文件中添加如下配置:

yaml

    spring.redis.host: 172.xx.xx.xx
    spring.redis.port: 6379
    spring.redis.database: 1
    spring.redis.password: 1234

 

以上配置用于连接到 Redis 服务器。 封装注解

封装一个注解@RequestLimit,使用方式如下:

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

        // 限制时间 单位:秒(默认值:一分钟)

        long period() default 60;

        // 允许请求的次数(默认值:20 次)

        long count() default 20;

    }

   

该注解用于标记需要限制 IP 访问次数的接口,并可以设置限制时间和允许请求的次数。

主体逻辑切面实现

使用 AOP 的切面来配合注解实现限流逻辑,代码如下:

    @Aspect
    @Component
    public class RequestLimitAspect {

 

        @Autowired
        StringRedisTemplate redisTemplate;

 

        private static final Logger logger = LoggerFactory.getLogger(RequestLimitAspect.class);

 

        private static final String blackListKey = "ai:black_list";

        // 切点
        @Pointcut("@annotation(requestLimit)")

        public void controllerAspect(RequestLimit requestLimit) { }

 

        @Around("controllerAspect(requestLimit)")
        public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {

            SseEmitter emitter = new SseEmitter(0L);

            // 获取当前请求 request 对象

            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

            if (attributes == null) {

                throw new IllegalStateException("在上下文中没有请求的属性,可能是非 web 程序访问。");

            }

            HttpServletRequest request = attributes.getRequest();

            long period = requestLimit.period();

            long limitCount = requestLimit.count();

 
            // 解析出来真实请求的 ip,防止多重代理 ip 攻击
            String ip = RequestUtil.getIpAdrress(request);  

            String uri = request.getRequestURI();

            String key = "ai:req_limit_" + uri + ":" + ip;

            // 检查用户 IP 是否在黑名单中

            BoundSetOperations<String, String> blackListOperations = redisTemplate.boundSetOps(blackListKey);

            if (blackListOperations.isMember(ip)) {

                logger.error("接口拦截:真实 IP 为{}, 已经在黑名单中", ip);

                // 这里被我 aop 环绕的接口返回值是 sse 类型,所以此处我也需要使用 sse 形式返回。根据你接口返回值来

                SseEmitter emitter = new SseEmitter(0L);

                emitter.send(SseEmitter.event().name(AppConsts.EVENT_ERROR).data("您的请求过于频繁,请于 5 分钟后再次访问"));

                emitter.complete();

                return emitter;

            }

 

            ZSetOperations zSetOperations = redisTemplate.opsForZSet();

 

            // 添加当前时间戳

            long currentMs = System.currentTimeMillis();

            // zSetOperations.add(key, currentMs, currentMs);

            zSetOperations.add(key, String.valueOf(currentMs), currentMs);

 

            // 设置用户的过期时间

            redisTemplate.expire(key, period, TimeUnit.SECONDS);

 

            // 删除当前窗口之外的值(如果时间窗口是 60 秒,那么在 60 秒内的同一 IP 请求会被计数,超过 60 秒的请求就不应该再被计数了,因为它们已经滑出时间窗口了)

            Long aLong = zSetOperations.removeRangeByScore(key, 0, currentMs - period * 1000);

 

            // 检查所有可用计数

            Long count = zSetOperations.zCard(key);

 

            if (count > limitCount) {

                logger.error("接口拦截:{} 请求超过限制频率【{}次/{}s】, 真实 IP 为{}", uri, limitCount, period, ip);

 

                // 将用户 IP 添加到黑名单并设置过期时间为 5 分钟(300 秒)

                blackListOperations.add(ip);

                redisTemplate.expire(blackListKey, 300, TimeUnit.SECONDS);

                SseEmitter emitter = new SseEmitter(0L);

                emitter.send(SseEmitter.event().name(AppConsts.EVENT_ERROR).data("系统繁忙,请稍后重试"));

                emitter.complete();

                return emitter;

            }

 

            // 如果条件不成立,将继续执行 controller 层的方法

            return joinPoint.proceed();

        }

    }

   

 

在该切面中,通过获取请求的 IP 和 URI 生成键,检查 IP 是否在黑名单中。如果不在黑名单中,则向 Redis 的 ZSet 中添加当前时间戳,并设置过期时间。然后删除当前窗口之外的值,并检查可用计数。如果计数超过限制次数,则将 IP 添加到黑名单中,并返回相应的提示信息;否则,继续执行 controller 层的方法。

 

添加注解到接口

在想要限流的接口上添加@RequestLimit注解,并设置相应的属性值。  

  @RequestLimit(count = 30)
  public Result knowledgeConverseEvents(QAAppDto dto, FacadeBase FacadeBase) {
        return appIntelligentAS.knowledgeConverseStream(dto);
    }`

  

3.优缺点

 

优点:

  1. 利用 Redis 的高并发和内存单线程优势,能够快速处理大量的请求,提高系统的性能。
  2. 通过注解的方式,可以方便地将限流逻辑应用到需要的接口上,具有较好的灵活性和可扩展性。
  3. 能够有效地防止恶意攻击和过度频繁的请求,保护系统的稳定性和安全性。

 

缺点:

  1. 依赖 Redis 服务器,如果 Redis 服务器出现故障,可能会影响限流功能的正常运行。
  2. 对于分布式系统,需要确保 Redis 服务器的高可用性和数据一致性。