SpringBoot 自定义注解和AOP实现基于IP的接口限流和黑白名单

在日常的开发中,为了保证系统的稳定性,很多时候需要做限流处理,它可以有效的防止恶意请求对系统造成过载,常见的限流方案有:

  • 网关限流:Ng,apisix等
  • 服务器端限流:服务端接口限流
  • 令牌桶算法:通过定期生成令牌放入桶中,请求需要消耗令牌才能通过
  • 熔断机制

初始化项目

先创建一个SpringBoot项目,添加必要依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

配置Redis配置

spring:
    #redis
    redis:
        # 地址
        host: 127.0.0.1
        # 端口,默认为6379
        port: 6379

自定义限流注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
	//限制次数
    int limit() default 5;
    //限制时间 秒
    int timeout() default 60;
}

编写限流切面

使用AOP实现限流逻辑,并增加IP黑白名单判断,使用Redis来存储和检查请求次数及黑白名单信息

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

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

@Aspect
@Component
public class RateLimitAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private HttpServletRequest request;
	//定义黑名单key前缀
    private static final String BLACKLIST_KEY_PREFIX = "blacklist:";
    //定义白名单key前缀
    private static final String WHITELIST_KEY_PREFIX = "whitelist:";

    @Around("@annotation(rateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
    	//获取IP
        String ip = request.getRemoteAddr();
        
        //黑名单则直接异常
        if (isBlacklisted(ip)) {
            throw new RuntimeException("超出访问限制已加入黑名单,1小时后再访问");
        }

		//如果是白名单下的不做限制
        if (isWhitelisted(ip)) {
            return joinPoint.proceed();
        }

        String key = generateKey(joinPoint, ip);
        int limit = rateLimit.limit();
        int timeout = rateLimit.timeout();
        
        String countStr = redisTemplate.opsForValue().get(key);
        int count = countStr == null ? 0 : Integer.parseInt(countStr);

        if (count < limit) {
            redisTemplate.opsForValue().set(key, String.valueOf(count + 1), timeout, TimeUnit.SECONDS);
            return joinPoint.proceed();
        } else {
            addToBlacklist(ip);
            throw new RuntimeException("超出请求限制IP已被列入黑名单");
        }
    }
private boolean isBlacklisted(String ip) {
        return redisTemplate.hasKey(BLACKLIST_KEY_PREFIX + ip);
    }

    private boolean isWhitelisted(String ip) {
        return redisTemplate.hasKey(WHITELIST_KEY_PREFIX + ip);
    }

    private void addToBlacklist(String ip) {
        redisTemplate.opsForValue().set(BLACKLIST_KEY_PREFIX + ip, "true", 1, TimeUnit.HOURS);
    }

    private String generateKey(ProceedingJoinPoint joinPoint, String ip) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getName();
        return className + ":" + methodName + ":" + ip;
    }
}

Controller 中使用限流注解

创建一个简单的限流测试Controller,并在需要限流的方法上使用 @RateLimit 注解:,需要编写异常处理,返回RateLimitAspect异常信息,并以字符串形式返回

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class TestController {

	//由于是简单的测试项目,这里就直接定义异常处理,并为采用全局异常处理
	@ExceptionHandler(value = Exception.class)
    public String handleException(Exception ex) {
        return ex.getMessage();
    }

    @RateLimit(limit = 5, timeout = 60)
    @GetMapping("/limit")
    public String testRateLimit() {
        return "Request successful!";
    }
}
posted @ 2025-09-19 22:21  小郑[努力版]  阅读(14)  评论(0)    收藏  举报