springboot自定义注解RateLimiter限流注解技术文档详解
目录
- 什么是限流
- 系统架构
- 核心组件详解
- 1. 限流注解 (@RateLimiter)
- 2. 限流类型枚举 (RateLimitType)
- 3. 限流异常类 (RateLimitException)
- 4. 全局异常处理器 (RateLimitExceptionHandler)
- 5. IP工具类 (IpUtils)
- 技术实现原理
- 1. AOP切面拦截
- 2. 缓存数据结构
- 完整代码示例
- 1. 控制器示例
- 2. 统一返回对象
- 使用指南
- 1. 基本使用
- 2. 不同场景的配置建议
- 3. 双重限流配置
- 总结
什么是限流
限流是一种控制系统访问频率的技术手段,就像高速公路的收费站控制车流量一样。
生活场景类比:
- 银行ATM机:每张卡每天最多取款5次
- 手机验证码:每个手机号每分钟最多发送1条
- 网站登录:每个IP每分钟最多尝试5次
技术价值:
- 防止恶意攻击:阻止暴力破解、恶意爬虫
- 保护系统稳定:避免瞬间大量请求压垮服务器
- 提升用户体验:确保正常用户的访问质量
- 节约成本:减少不必要的资源消耗
系统架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 用户请求 │───→│ 限流切面 │───→│ 业务接口 │ │ (HTTP API) │ │ (AOP拦截) │ │ (Controller) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼ ┌─────────────────┐ │ 限流服务 │ │ (核心逻辑处理) │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ 缓存存储 │ │ (EhCache/Redis) │ └─────────────────┘
工作流程:
- 用户发起HTTP请求
- Spring AOP切面拦截带有@RateLimiter注解的方法
- 限流服务根据注解配置生成限流键
- 从缓存中获取当前访问次数
- 判断是否超过限制,决定放行或拒绝
- 更新缓存中的计数器
核心组件详解
1. 限流注解 (@RateLimiter)
这是系统的核心注解,定义了限流的各种参数:
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 { /** * 缓存前缀 - 用于区分不同业务的限流数据 */ String prefix() default "jblimit:"; /** * 时间窗口(秒) - 限流的时间范围 */ int time() default 60; /** * 允许访问次数 - 时间窗口内最大访问次数 */ @AliasFor(attribute = "count") int value() default 12; /** * 限制类型 - 决定按什么维度限流 */ RateLimitType limitType() default RateLimitType.DEFAULT; /** * 限制提示消息 - 触发限流时返回的错误信息 */ String msg() default "操作过于频繁,请稍后重试"; /** * 允许访问次数 - 与value互为别名 */ @AliasFor(attribute = "value") int count() default 12; /** * 自定义键 - 当limitType为CUSTOM时使用 */ String customKey() default ""; /** * 是否启用 - 可用于动态开关限流功能 */ boolean enabled() default true; /** * 额外的时间窗口限制(秒) * 实现双重限流:比如1秒最多1次 + 1分钟最多10次 */ int extraTime() default -1; /** * 额外时间窗口内的允许访问次数 */ int extraCount() default -1; /** * 额外限制的提示消息 */ String extraMsg() default ""; }
2. 限流类型枚举 (RateLimitType)
package cn.jbolt.config.anno.rateLimiter; public enum RateLimitType { /** * 默认限制(全局) * 所有请求共享一个计数器 */ DEFAULT, /** * 基于IP地址限制 * 每个IP独立计数 */ IP, /** * 基于用户ID限制 * 每个登录用户独立计数 */ USER, /** * 基于自定义KEY限制 * 根据业务逻辑自定义限流维度 */ 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.putphp("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"); android 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); } }
技术实现原理
1. AOP切面拦截
系统使用Spring AOP在方法执行前进行拦截,这是一个核心的限流切面类:
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(http://www.devze.com)) { return point.proceed(); } // 获取HTTP请求对象 HttpServletRequest request = getCurrentRequest(); if (request == null) { logger.warn("无法获取HttpServletRequest,跳过限流检查"); return point.proceed(); } // 生成限流键 String limitKey = generateLimitKey(point, rateLimiter, request); // 执行主要限流检查 checkRateLimit(limitKey, rateLimiter.time(), rateLimiter.count(), rateLimiter.msg()); // 执行额外限流检查(如果配置了) 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); } // 所有限流检查通过,继续执行业务方法 return point.proceed(); } /** * 执行限流检查 */ private void checkRateLimit(String key, int timeWindow, int maxCount, String message) { try { // 增加计数器并获取当前访问次数 int currentCount = RateLimiterCache.incrementAndGet(key, timeWindow, TimeUnit.SECONDS); logger.debug("限流检查: key={}, 当前次数={}, 限制次数={}", key, currentCount, maxCount); // 检查是否超过限制 if (currentCount > maxCount) { long ttl = RateLimiterCache.getTtl(key); logger.warn("触发限流: key={}, 当前次数={}, 限制次数={}, 剩余时间={}秒", 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); // 根据限流类型添加不同的标识 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 * 这里需要根据实际的用户认证体系来实现 */ 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:从请求参数中获取 String userIdParam = request.getParameter("userId"); if (userIdParam != null) { return userIdParam; } return null; } }
2. 缓存数据结构
系统使用一个包装类来存储缓存数据:
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("用户名或密码错误"); } } /** * 发送验证码 - 防止恶意发送 * 每个IP每分钟最多3次 */ @PostMapping("/sms/send") @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 3, msg = "验证码发送过于频繁,请稍后重试" ) public Result sendSms(@RequestBody SmsRequest request) { // 发送短信逻辑 boolean success = smsService.sendCode(request.getPhone()); return success ? Result.success("发送成功") : Result.error("发送失败"); } /** * 查询接口 - 防止爬虫 * 每个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); } js /** * 用户操作 - 防止频繁操作 * 每个用户每分钟最多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分钟内的操作次数已达上限" ) 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) { // 创建订单逻辑 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. 统一返回对象
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"; } // 自定义时间窗口和次数 @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 = "操作过于频繁,请稍后重试" ) // 全局保护 - 系统级限制 @RateLimiter( limitType = RateLimitType.DEFAULT, time = 60, count = 200, msg = "系统繁忙,请稍后重试" )
3. 双重限流配置
// 严格的双重限流:秒级 + 分钟级 @RateLimiter( limitType = RateLimitType.IP, time = 1, count = 1, msg = "请求过于频繁,请稍后再试", extraTime = 60, extraCount = 10, extraMsg = "您在1分钟内的请求次数已达上限" ) // 适中的双重限流:分钟级 + 小时级 @RateLimiter( limitType = RateLimitType.IP, time = 60, count = 100, msg = "1分钟内请求过多", extraTime = 3600, extraCount = 1000, extraMsg = "1小时内请求过多" )
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(wwlbbhqw.cppcns.com)。
精彩评论