springboot自定義注解RateLimiter限流注解技術文檔詳解
更新時間:2025年07月25日 15:44:14 作者:yzhSWJ
文章介紹了限流技術的概念、作用及實現(xiàn)方式,通過Spring AOP攔截方法、緩存存儲計數(shù)器,結合注解、枚舉、異常類等核心組件,實現(xiàn)對訪問頻率的控制,旨在防止攻擊、保障系統(tǒng)穩(wěn)定、提升用戶體驗及節(jié)約成本
什么是限流
限流是一種控制系統(tǒng)訪問頻率的技術手段,就像高速公路的收費站控制車流量一樣。
生活場景類比:
- 銀行ATM機:每張卡每天最多取款5次
- 手機驗證碼:每個手機號每分鐘最多發(fā)送1條
- 網(wǎng)站登錄:每個IP每分鐘最多嘗試5次
技術價值:
- 防止惡意攻擊:阻止暴力破解、惡意爬蟲
- 保護系統(tǒng)穩(wěn)定:避免瞬間大量請求壓垮服務器
- 提升用戶體驗:確保正常用戶的訪問質(zhì)量
- 節(jié)約成本:減少不必要的資源消耗
系統(tǒng)架構
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 用戶請求 │───→│ 限流切面 │───→│ 業(yè)務接口 │ │ (HTTP API) │ │ (AOP攔截) │ │ (Controller) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼ ┌─────────────────┐ │ 限流服務 │ │ (核心邏輯處理) │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ 緩存存儲 │ │ (EhCache/Redis) │ └─────────────────┘
工作流程:
- 用戶發(fā)起HTTP請求
- Spring AOP切面攔截帶有@RateLimiter注解的方法
- 限流服務根據(jù)注解配置生成限流鍵
- 從緩存中獲取當前訪問次數(shù)
- 判斷是否超過限制,決定放行或拒絕
- 更新緩存中的計數(shù)器
核心組件詳解
1. 限流注解 (@RateLimiter)
這是系統(tǒng)的核心注解,定義了限流的各種參數(shù):
package cn.jbolt.config.anno.rateLimiter; import org.springframework.core.annotation.AliasFor; 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.TYPE, ElementType.METHOD}) public @interface RateLimiter { /** * 緩存前綴 - 用于區(qū)分不同業(yè)務的限流數(shù)據(jù) */ String prefix() default "jblimit:"; /** * 時間窗口(秒) - 限流的時間范圍 */ int time() default 60; /** * 允許訪問次數(shù) - 時間窗口內(nèi)最大訪問次數(shù) */ @AliasFor(attribute = "count") int value() default 12; /** * 限制類型 - 決定按什么維度限流 */ RateLimitType limitType() default RateLimitType.DEFAULT; /** * 限制提示消息 - 觸發(fā)限流時返回的錯誤信息 */ String msg() default "操作過于頻繁,請稍后重試"; /** * 允許訪問次數(shù) - 與value互為別名 */ @AliasFor(attribute = "value") int count() default 12; /** * 自定義鍵 - 當limitType為CUSTOM時使用 */ String customKey() default ""; /** * 是否啟用 - 可用于動態(tài)開關限流功能 */ boolean enabled() default true; /** * 額外的時間窗口限制(秒) * 實現(xiàn)雙重限流:比如1秒最多1次 + 1分鐘最多10次 */ int extraTime() default -1; /** * 額外時間窗口內(nèi)的允許訪問次數(shù) */ int extraCount() default -1; /** * 額外限制的提示消息 */ String extraMsg() default ""; }
2. 限流類型枚舉 (RateLimitType)
package cn.jbolt.config.anno.rateLimiter; public enum RateLimitType { /** * 默認限制(全局) * 所有請求共享一個計數(shù)器 */ DEFAULT, /** * 基于IP地址限制 * 每個IP獨立計數(shù) */ IP, /** * 基于用戶ID限制 * 每個登錄用戶獨立計數(shù) */ USER, /** * 基于自定義KEY限制 * 根據(jù)業(yè)務邏輯自定義限流維度 */ CUSTOM }
3. 限流異常類 (RateLimitException)
package cn.jbolt.config.exception; public class RateLimitException extends RuntimeException { private final String message; private final int retryAfter; public RateLimitException(String message) { this(message, 0); } public RateLimitException(String message, int retryAfter) { super(message); this.message = message; this.retryAfter = retryAfter; } @Override public String getMessage() { return message; } public int getRetryAfter() { return retryAfter; } }
4. 全局異常處理器 (RateLimitExceptionHandler)
package cn.jbolt.config.handler; import cn.jbolt.config.exception.RateLimitException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class RateLimitExceptionHandler { @ExceptionHandler(RateLimitException.class) public ResponseEntity<Map<String, Object>> handleRateLimitException( RateLimitException e, HttpServletResponse response) { Map<String, Object> result = new HashMap<>(); result.put("code", HttpStatus.TOO_MANY_REQUESTS.value()); result.put("message", e.getMessage()); result.put("data", null); // 設置HTTP響應頭,告訴客戶端多久后可以重試 if (e.getRetryAfter() > 0) { response.setHeader("Retry-After", String.valueOf(e.getRetryAfter())); } response.setHeader("X-RateLimit-Window", "60"); return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(result); } }
5. IP工具類 (IpUtils)
package cn.jbolt.util; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; public class IpUtils { private static final String[] IP_HEADER_NAMES = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR" }; private static final String UNKNOWN = "unknown"; private static final String LOCALHOST_IPV4 = "127.0.0.1"; private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1"; /** * 獲取客戶端真實IP地址 * 處理代理服務器、負載均衡器等場景 */ public static String getClientIp(HttpServletRequest request) { if (request == null) { return UNKNOWN; } String ip = null; // 依次檢查各種可能的IP頭 for (String header : IP_HEADER_NAMES) { ip = request.getHeader(header); if (isValidIp(ip)) { break; } } // 如果頭信息中沒有找到,則使用getRemoteAddr if (!isValidIp(ip)) { ip = request.getRemoteAddr(); if (LOCALHOST_IPV6.equals(ip)) { ip = LOCALHOST_IPV4; } } // 處理多個IP的情況(X-Forwarded-For可能包含多個IP) if (StringUtils.hasText(ip) && ip.contains(",")) { ip = ip.split(",")[0].trim(); } return StringUtils.hasText(ip) ? ip : UNKNOWN; } /** * 檢查IP是否有效 */ private static boolean isValidIp(String ip) { return StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip); } }
技術實現(xiàn)原理
1. AOP切面攔截
系統(tǒng)使用Spring AOP在方法執(zhí)行前進行攔截,這是一個核心的限流切面類:
package cn.jbolt.config.aspect; import cn.jbolt.config.anno.rateLimiter.RateLimiter; import cn.jbolt.config.anno.rateLimiter.RateLimitType; import cn.jbolt.config.exception.RateLimitException; import cn.jbolt.util.IpUtils; import cn.jbolt.util.cache.RateLimiterCache; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.TimeUnit; @Aspect @Component public class RateLimiterAspect { private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class); @Around("@annotation(rateLimiter)") public Object around(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable { // 檢查是否啟用限流 if (!rateLimiter.enabled()) { return point.proceed(); } // 獲取HTTP請求對象 HttpServletRequest request = getCurrentRequest(); if (request == null) { logger.warn("無法獲取HttpServletRequest,跳過限流檢查"); return point.proceed(); } // 生成限流鍵 String limitKey = generateLimitKey(point, rateLimiter, request); // 執(zhí)行主要限流檢查 checkRateLimit(limitKey, rateLimiter.time(), rateLimiter.count(), rateLimiter.msg()); // 執(zhí)行額外限流檢查(如果配置了) if (rateLimiter.extraTime() > 0 && rateLimiter.extraCount() > 0) { String extraLimitKey = limitKey + ":extra"; String extraMsg = rateLimiter.extraMsg().isEmpty() ? rateLimiter.msg() : rateLimiter.extraMsg(); checkRateLimit(extraLimitKey, rateLimiter.extraTime(), rateLimiter.extraCount(), extraMsg); } // 所有限流檢查通過,繼續(xù)執(zhí)行業(yè)務方法 return point.proceed(); } /** * 執(zhí)行限流檢查 */ private void checkRateLimit(String key, int timeWindow, int maxCount, String message) { try { // 增加計數(shù)器并獲取當前訪問次數(shù) int currentCount = RateLimiterCache.incrementAndGet(key, timeWindow, TimeUnit.SECONDS); logger.debug("限流檢查: key={}, 當前次數(shù)={}, 限制次數(shù)={}", key, currentCount, maxCount); // 檢查是否超過限制 if (currentCount > maxCount) { long ttl = RateLimiterCache.getTtl(key); logger.warn("觸發(fā)限流: key={}, 當前次數(shù)={}, 限制次數(shù)={}, 剩余時間={}秒", key, currentCount, maxCount, ttl); throw new RateLimitException(message, (int) ttl); } } catch (RateLimitException e) { throw e; } catch (Exception e) { logger.error("限流檢查異常: key={}", key, e); // 限流服務異常時,選擇放行而不是阻塞 } } /** * 生成限流鍵 */ private String generateLimitKey(ProceedingJoinPoint point, RateLimiter rateLimiter, HttpServletRequest request) { StringBuilder keyBuilder = new StringBuilder(); keyBuilder.append(rateLimiter.prefix()); // 添加方法簽名 String methodSignature = point.getSignature().toShortString(); keyBuilder.append(methodSignature); // 根據(jù)限流類型添加不同的標識 switch (rateLimiter.limitType()) { case IP: keyBuilder.append(":ip:").append(IpUtils.getClientIp(request)); break; case USER: String userId = getCurrentUserId(request); keyBuilder.append(":user:").append(userId != null ? userId : "anonymous"); break; case CUSTOM: keyBuilder.append(":custom:").append(rateLimiter.customKey()); break; case DEFAULT: default: keyBuilder.append(":default:global"); break; } // 添加時間窗口,確保不同時間窗口的限流獨立 keyBuilder.append(":").append(rateLimiter.time()); String finalKey = keyBuilder.toString(); logger.debug("生成限流鍵: {}", finalKey); return finalKey; } /** * 獲取當前HTTP請求 */ private HttpServletRequest getCurrentRequest() { try { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return attrs != null ? attrs.getRequest() : null; } catch (Exception e) { logger.warn("獲取HttpServletRequest失敗", e); return null; } } /** * 獲取當前用戶ID * 這里需要根據(jù)實際的用戶認證體系來實現(xiàn) */ private String getCurrentUserId(HttpServletRequest request) { // 方案1:從Session中獲取 Object userId = request.getSession().getAttribute("userId"); if (userId != null) { return userId.toString(); } // 方案2:從JWT Token中獲取 String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { // 解析JWT獲取用戶ID // return JwtUtils.getUserIdFromToken(token); } // 方案3:從請求參數(shù)中獲取 String userIdParam = request.getParameter("userId"); if (userIdParam != null) { return userIdParam; } return null; } }
2. 緩存數(shù)據(jù)結構
系統(tǒng)使用一個包裝類來存儲緩存數(shù)據(jù):
package cn.jbolt.util.cache; import java.io.Serializable; import java.util.concurrent.TimeUnit; public class CacheWrapper implements Serializable { private static final long serialVersionUID = 1L; private Object value; private long timestamp; private long durationMillis; public CacheWrapper() { } public CacheWrapper(Object value, long duration, TimeUnit unit) { this.value = value; this.timestamp = System.currentTimeMillis(); this.durationMillis = unit.toMillis(duration); } /** * 檢查是否已過期 */ public boolean isExpired() { return System.currentTimeMillis() - timestamp > durationMillis; } /** * 獲取剩余過期時間(毫秒) */ public long getRemainingTime() { long elapsed = System.currentTimeMillis() - timestamp; return Math.max(0, durationMillis - elapsed); } // getter和setter方法 public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } public long getDurationMillis() { return durationMillis; } public void setDurationMillis(long durationMillis) { this.durationMillis = durationMillis; } }
完整代碼示例
1. 控制器示例
package cn.jbolt.controller; import cn.jbolt.config.anno.rateLimiter.RateLimiter; import cn.jbolt.config.anno.rateLimiter.RateLimitType; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api") public class DemoController { /** * 登錄接口 - 防止暴力破解 * 每個IP每分鐘最多嘗試5次 */ @PostMapping("/login") @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 5, msg = "登錄嘗試過于頻繁,請1分鐘后重試" ) public Result login(@RequestBody LoginRequest request) { // 登錄邏輯 if (isValidUser(request.getUsername(), request.getPassword())) { return Result.success("登錄成功"); } else { return Result.error("用戶名或密碼錯誤"); } } /** * 發(fā)送驗證碼 - 防止惡意發(fā)送 * 每個IP每分鐘最多3次 */ @PostMapping("/sms/send") @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 3, msg = "驗證碼發(fā)送過于頻繁,請稍后重試" ) public Result sendSms(@RequestBody SmsRequest request) { // 發(fā)送短信邏輯 boolean success = smsService.sendCode(request.getPhone()); return success ? Result.success("發(fā)送成功") : Result.error("發(fā)送失敗"); } /** * 查詢接口 - 防止爬蟲 * 每個IP每分鐘最多100次 */ @GetMapping("/products") @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 100, msg = "查詢過于頻繁,請稍后重試" ) public Result getProducts(@RequestParam(defaultValue = "1") int page) { // 查詢商品邏輯 List<Product> products = productService.getProducts(page); return Result.success(products); } /** * 用戶操作 - 防止頻繁操作 * 每個用戶每分鐘最多30次 */ @PostMapping("/user/update") @RateLimiter( limitType = RateLimitType.USER, time = 60, count = 30, msg = "操作過于頻繁,請稍后重試" ) public Result updateUser(@RequestBody UserUpdateRequest request) { // 更新用戶信息邏輯 boolean success = userService.updateUser(request); return success ? Result.success("更新成功") : Result.error("更新失敗"); } /** * 關鍵操作 - 嚴格限流 * 1秒最多1次 + 1分鐘最多5次 */ @PostMapping("/transfer") @RateLimiter( limitType = RateLimitType.USER, time = 1, count = 1, msg = "操作過于頻繁,請稍后再試", extraTime = 60, extraCount = 5, extraMsg = "您在1分鐘內(nèi)的操作次數(shù)已達上限" ) public Result transfer(@RequestBody TransferRequest request) { // 轉賬邏輯 boolean success = transferService.transfer(request); return success ? Result.success("轉賬成功") : Result.error("轉賬失敗"); } /** * 自定義限流 - 按商品限制 * 每個商品每分鐘最多下單20次 */ @PostMapping("/order/{productId}") @RateLimiter( limitType = RateLimitType.CUSTOM, customKey = "product_order", time = 60, count = 20, msg = "該商品下單過于頻繁,請稍后重試" ) public Result createOrder(@PathVariable String productId, @RequestBody OrderRequest request) { // 創(chuàng)建訂單邏輯 Order order = orderService.createOrder(productId, request); return Result.success(order); } // 輔助方法 private boolean isValidUser(String username, String password) { // 實際的用戶驗證邏輯 return "admin".equals(username) && "123456".equals(password); } }
2. 統(tǒng)一返回對象
package cn.jbolt.common; public class Result { private int code; private String message; private Object data; public static Result success(Object data) { Result result = new Result(); result.code = 200; result.message = "success"; result.data = data; return result; } public static Result error(String message) { Result result = new Result(); result.code = 500; result.message = message; result.data = null; return result; } // getter和setter方法 public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
使用指南
1. 基本使用
// 最簡單的用法 - 使用默認配置 @RateLimiter(limitType = RateLimitType.IP) public String simpleApi() { return "success"; } // 自定義時間窗口和次數(shù) @RateLimiter( limitType = RateLimitType.IP, time = 60, // 60秒 count = 100 // 最多100次 ) public String customApi() { return "success"; }
2. 不同場景的配置建議
// 登錄接口 - 嚴格限制 @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 5, msg = "登錄嘗試過于頻繁,請1分鐘后重試" ) // 查詢接口 - 適中限制 @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 100, msg = "查詢過于頻繁,請稍后重試" ) // 用戶操作 - 按用戶限制 @RateLimiter( limitType = RateLimitType.USER, time = 60, count = 30, msg = "操作過于頻繁,請稍后重試" ) // 全局保護 - 系統(tǒng)級限制 @RateLimiter( limitType = RateLimitType.DEFAULT, time = 60, count = 200, msg = "系統(tǒng)繁忙,請稍后重試" )
3. 雙重限流配置
// 嚴格的雙重限流:秒級 + 分鐘級 @RateLimiter( limitType = RateLimitType.IP, time = 1, count = 1, msg = "請求過于頻繁,請稍后再試", extraTime = 60, extraCount = 10, extraMsg = "您在1分鐘內(nèi)的請求次數(shù)已達上限" ) // 適中的雙重限流:分鐘級 + 小時級 @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 100, msg = "1分鐘內(nèi)請求過多", extraTime = 3600, extraCount = 1000, extraMsg = "1小時內(nèi)請求過多" )
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Mybatis配置之properties和settings標簽的用法
這篇文章主要介紹了Mybatis配置之properties和settings標簽的用法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Java Map 在put值時value值不被覆蓋的解決辦法
這篇文章主要介紹了Java Map 在put值時value值不被覆蓋的解決辦法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-04-04詳解Spring Cloud中Hystrix 線程隔離導致ThreadLocal數(shù)據(jù)丟失
這篇文章主要介紹了詳解Spring Cloud中Hystrix 線程隔離導致ThreadLocal數(shù)據(jù)丟失,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03