使用 Spring Boot 自定義注解和AOP實現基于IP的接口限流和黑白名單
在我們日常開發的項目中為了保證系統的穩定性,很多時候我們需要對系統做限流處理,它可以有效防止惡意請求對系統造成過載。常見的限流方案主要有:
網關限流: NGINX、Zuul 等 API 網關
服務器端限流: 服務端接口限流
令牌桶算法: 通過定期生成令牌放入桶中,請求需要消耗令牌才能通過
熔斷機制: Hystrix、Resilience4j 等
本文將詳細介紹 Spring Boot 通過自定義注解和 AOP(面向切面編程),實現基于 IP 的限流和黑白名單功能,包括如何使用 Redis 存儲限流和黑名單信息。
項目搭建
添加必要的依賴。在 pom.xml 文件中添加以下內容:
<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>
配置 application.yml 加入 redis 配置
spring:#redisredis:# 地址host: 127.0.0.1# 端口,默認為6379port: 6379# 數據庫索引database: 0# 密碼password: password# 連接超時時間timeout: 10slettuce:pool:# 連接池中的最小空閑連接min-idle: 0# 連接池中的最大空閑連接max-idle: 8# 連接池的最大數據庫連接數max-active: 8# #連接池最大阻塞等待時間(使用負值表示沒有限制)max-wait: -1ms
自定義限流注解
創建一個自定義注解 RateLimit :
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 {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate 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();/*** 沒有經過代理使用: request.getRemoteAddr();*經過nginx代理使用: request.getHeader("X-Real-IP");**/String ip =IpUtil.getIpAddress(request); //黑名單則直接異常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);}// 添加ip到白名單內private void addToBlacklist(String ip) {redisTemplate.opsForValue().set(BLACKLIST_KEY_PREFIX + ip, "true", 1, TimeUnit.HOURS);}// redis key 拼接private String generateKey(ProceedingJoinPoint joinPoint, String ip) {String methodName = joinPoint.getSignature().getName();String className = joinPoint.getTarget().getClass().getName();return className + ":" + methodName + ":" + ip;}
}
/*** IP工具類*/
public class IpUtil {/*** 獲取ip* @param request 請求* @return {@link String }*/public static String getIpAddress(HttpServletRequest request) {String ipAddress = null;try {ipAddress = request.getHeader("x-forwarded-for");if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("WL-Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getRemoteAddr();if (ipAddress.equals("127.0.0.1")) {// 根據網卡取本機配置的IPInetAddress inet = null;try {inet = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}ipAddress = inet.getHostAddress();}}// 對于通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()// = 15if (ipAddress.indexOf(",") > 0) {ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));}}} catch (Exception e) {ipAddress="";}// ipAddress = this.getRequest().getRemoteAddr();return ipAddress;}/*** 獲取網關ip* @param request 請求* @return {@link String }*/public static String getGatwayIpAddress(ServerHttpRequest request) {HttpHeaders headers = request.getHeaders();String ip = headers.getFirst("x-forwarded-for");if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {// 多次反向代理后會有多個ip值,第一個ip才是真實ipif (ip.indexOf(",") != -1) {ip = ip.split(",")[0];}}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = headers.getFirst("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = headers.getFirst("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = headers.getFirst("HTTP_CLIENT_IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = headers.getFirst("HTTP_X_FORWARDED_FOR");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = headers.getFirst("X-Real-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddress().getAddress().getHostAddress();}return 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 {@RateLimit(limit = 5, timeout = 60)@GetMapping("/limit")public String testRateLimit() {return "Request successful!";}
}