项目开发中,需要防止用户频繁请求接口(如登录、注册、短信验证码等),提升系统安全性与稳定性。可以通过 Redis + 拦截器/AOP + 自定义注解 实现接口防刷功能(即 限流防刷)。

一、实现思路

  • 自定义注解:标记需要防刷的controller层接口,配置限流规则(如:每 5 秒最多 3 次)。

  • 自定义AOP :在请求执行前进行拦截,判断是否超过频率限制。

  • Redis:存储访问计数(可以设定IP:接口路径 或 用户ID:接口等规则),利用 INCR 和 EXPIRE 实现滑动时间窗口计数。

  • 拒绝策略:超过阈值则返回“请求过于频繁”。

二、实际代码

1、创建自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LimitRateByAop {

    //限流key的前缀
    String prefix() default "rate_limit:";

    //时间窗口(秒)
    int timeOut() default 60;

    //最大请求次数
    int maxCount() default 3;

    //限流后提示内容
    String message() default "请求次数过多,请稍后再试!";

    //限流键的生成策略
    KeyType keyType() default KeyType.IP;
    enum KeyType{
        IP,
        USER,
        PATH,
        IP_PATH
    }
}
2、实现AOP切面类
@Aspect
@Component
@Slf4j
public class MyRateLimitAspect {

    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    @Around("@annotation(com.mdx.annotation.LimitRateByAop)")
    public Object around (ProceedingJoinPoint proceedingJoinPoint){
        try {
            //获取请求对象
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();

            //判断方法上是否有MyRateLimitByAop注解
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
            Method method = signature.getMethod();
            LimitRateByAop limitRateByAop = method.getAnnotation(LimitRateByAop.class);
            //未被标注,直接放过去
            if(Objects.isNull(limitRateByAop)){
                return proceedingJoinPoint.proceed();
            }

            //获取注解参数
            int timeWindow = limitRateByAop.timeOut();
            int maxCount = limitRateByAop.maxCount();
            String message = limitRateByAop.message();

            String key =generateKey(limitRateByAop, request, method);
            if(Strings.isBlank(key)){
                //无法生成key,跳过
                return proceedingJoinPoint.proceed();
            }

            // 自增计数(Redis会自动初始化不存在的Key为0,再自增为1)
            Long count = redisTemplate.opsForValue().increment(key, 1);
            // 处理count为null的情况(Redis连接异常等)
            if (count == null) {
                log.warn("Redis自增操作返回null,key:{}", key);
                return proceedingJoinPoint.proceed();
            }

            // 首次计数(Key刚创建),设置过期时间
            if (count == 1) {
                redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS);
                log.debug("限流Key首次创建,设置过期时间,key:{}, timeWindow:{}秒", key, timeWindow);
            }

            // 限流判断:计数超过最大限制,返回提示信息
            if (count > maxCount) {
                log.warn("接口限流触发,key:{}, 当前计数:{}, 最大限制:{}", key, count, maxCount);
                // 建议返回统一格式的响应对象(而非直接返回字符串),示例:
                // return Result.fail(429, message);
                return message;
            }

            // 放行,执行目标方法
            return proceedingJoinPoint.proceed();

        } catch (Throwable e) {
            throw new RuntimeException("Aop实现限流报错:{}",e.getCause());
        }

    }

    private String generateKey(LimitRateByAop limitRateByAop, HttpServletRequest serverRequest, Method method){

        String prefix = limitRateByAop.prefix();
        LimitRateByAop.KeyType keyType = limitRateByAop.keyType();
        String ip = getIpAddre(serverRequest);
        String uri = serverRequest.getRequestURI();

        String key = "";

        switch (keyType){
            case IP: key = prefix + ":" + ip;
            case USER: key = prefix + ":" + "user";
            case PATH: key = prefix + ":" + uri;
            case IP_PATH: key = prefix + ":" + ip + ":" + uri;
        }

        return key;

    }

    private String getIpAddre(HttpServletRequest servletRequest){
        String ip = null;

        //X-Forwarded-For: 经过层层代理后,最终客户端真实IP在最前面
        String  xForwardedFor = servletRequest.getHeader("X-Forwarded-For");
        if(Strings.isNotBlank(xForwardedFor) && "unKnown".equalsIgnoreCase(xForwardedFor)){
            ip = xForwardedFor.split(",")[0].trim();
        }

        // Proxy-Client-IP: WebLogic代理服务器IP
        if(Strings.isBlank(ip)){
            ip = servletRequest.getHeader("Proxy-Client-IP");
        }

        //WL-Proxy-Client-IP: WebLogic代理服务器IP
        if(Strings.isBlank(ip)){
            ip = servletRequest.getHeader("WL-Proxy-Client-IP");
        }

        //获取远程地址
        if(Strings.isBlank(ip)){
            ip = servletRequest.getRemoteAddr();
        }

        // 处理IPv6本地回环地址
        if("0:0:0:0:0:0:0:1".equals(ip)) {
            ip ="127.0.0.1";
        }

        return ip;
    }
}
3、controller层创建接口
 
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("sendSms")
    @LimitRateByAop(
            prefix = "rate_limit:sms",
            timeOut = 5,
            maxCount = 3,
            keyType = LimitRateByAop.KeyType.IP_PATH
    )
    public String limitRateByAop(){
        System.out.println("发送短信");
        return "发送短信成功";
    }


}

三、结果校验

输入地址:localhost:8021/user/sendSms后,可以查看发送短信成功,之后在5秒内连续刷新超过设置的最大访问次数,会触发接口限流,返回设置的提示信息。

1、首次刷新成功

2、5秒内连续刷新超过3次,触发限流

总结:

通过自定义注解@LimitRateByAop标记需要限流的接口,配置时间窗口和最大请求次数等参数。AOP切面类利用Redis实现滑动时间窗口计数,通过INCR和EXPIRE命令进行访问计数和过期时间设置。当请求超过阈值时返回提示信息,否则放行请求。

如果对大家有帮助的话,点个免费的赞和关注。防止找不到,可以点击收藏呦。
 

更多推荐