根据ip限制接口访问次数

发布于:2024-05-15 ⋅ 阅读:(145) ⋅ 点赞:(0)

前言

我们利用redis去实现这个功能,redis的天然高并发和内存单线程速度拉满,非常适合做这个场景。为了可用性,我们把它封装成注解形式,哪个接口想被根据ip限制接口访问次数,直接标注上注解即可。

一、添加配置

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

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

二、封装注解

封装一个注解使用,并且给一个默认值,防止空指针异常。

package com.xxx.ai.intelligentqa.annotate;

import java.lang.annotation.*;

/**
 * 接口访问频率注解,默认一分钟只能访问20次
 */

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

    // 限制时间 单位:秒(默认值:一分钟)
    long period() default 60;

    // 允许请求的次数(默认值:20次)
    long count() default 20;
}

三、主体逻辑切面实现

我们利用AOP的切面,来配合注解实现限流逻辑:

package com.xxx.ai.intelligentqa.aop;

import com.xxx.ai.intelligentqa.annotate.RequestLimit;
import com.xxx.ai.intelligentqa.tools.RequestUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;


@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();

        String ip = RequestUtil.getIpAdrress(request); //解析出来真是请求的ip,防止多重代理ip攻击
        String uri = request.getRequestURI();
        String key = "ai:req_limit_".concat(uri).concat(":").concat(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();
    }
}

四、把注解加在想要限流的接口上

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

网站公告

今日签到

点亮在社区的每一天
去签到