SpringBoot實現(xiàn)接口防刷的五種方案
1. 基于注解的訪問頻率限制
最常見的防刷方案是通過自定義注解和AOP切面實現(xiàn)訪問頻率限制。這種方法簡單易用,實現(xiàn)成本低。
實現(xiàn)步驟
1.1 創(chuàng)建限流注解
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimit { /** * 限制時間段,單位為秒 */ int time() default 60; /** * 在限制時間段內(nèi)允許的最大請求次數(shù) */ int count() default 10; /** * 限流的key,支持SpEL表達式 */ String key() default ""; /** * 提示信息 */ String message() default "操作太頻繁,請稍后再試"; }
1.2 實現(xiàn)限流切面
@Aspect @Component @Slf4j public class RateLimitAspect { @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable { // 獲取請求的方法名 String methodName = pjp.getSignature().getName(); // 獲取請求的類名 String className = pjp.getTarget().getClass().getName(); // 組合限流key String limitKey = getLimitKey(pjp, rateLimit, methodName, className); // 獲取限流參數(shù) int time = rateLimit.time(); int count = rateLimit.count(); // 執(zhí)行限流邏輯 boolean limited = isLimited(limitKey, time, count); if (limited) { throw new RuntimeException(rateLimit.message()); } // 執(zhí)行目標方法 return pjp.proceed(); } private String getLimitKey(ProceedingJoinPoint pjp, RateLimit rateLimit, String methodName, String className) { // 獲取用戶自定義的key String key = rateLimit.key(); if (StringUtils.hasText(key)) { // 支持SpEL表達式解析 StandardEvaluationContext context = new StandardEvaluationContext(); MethodSignature signature = (MethodSignature) pjp.getSignature(); String[] parameterNames = signature.getParameterNames(); Object[] args = pjp.getArgs(); for (int i = 0; i < parameterNames.length; i++) { context.setVariable(parameterNames[i], args[i]); } ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(key); key = expression.getValue(context, String.class); } else { // 默認使用類名+方法名+IP地址作為key HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ip = getIpAddress(request); key = ip + ":" + className + ":" + methodName; } return "rate_limit:" + key; } private boolean isLimited(String key, int time, int count) { // 使用Redis的計數(shù)器實現(xiàn)限流 try { Long currentCount = redisTemplate.opsForValue().increment(key, 1); // 如果是第一次訪問,設(shè)置過期時間 if (currentCount == 1) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return currentCount > count; } catch (Exception e) { log.error("限流異常", e); return false; } } private String getIpAddress(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
1.3 使用示例
@RestController @RequestMapping("/api") public class UserController { @RateLimit(time = 60, count = 3, message = "請求太頻繁,請稍后再試") @GetMapping("/user/{id}") public User getUser(@PathVariable Long id) { return userService.getUser(id); } // 使用SpEL表達式指定key @RateLimit(time = 60, count = 1, key = "#id + '_' + #request.remoteAddr") @PostMapping("/user/{id}/update") public Result updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO, HttpServletRequest request) { return userService.updateUser(id, userDTO); } }
優(yōu)缺點分析
優(yōu)點:
- 實現(xiàn)簡單,上手容易,單機情況下可以去掉Redis換成本地緩存實現(xiàn)
- 注解式使用,對業(yè)務(wù)代碼無侵入
- 可以精確控制接口粒度
- 支持靈活的限流策略配置
缺點:
- 限流邏輯相對簡單,無法應(yīng)對復(fù)雜場景
- 缺少預(yù)警機制
2. 令牌桶算法實現(xiàn)限流
令牌桶算法是一種更加靈活的限流算法,可以允許突發(fā)流量,同時又能限制長期的平均流量。
實現(xiàn)步驟
2.1 引入依賴
Google提供的Guava庫中包含了令牌桶實現(xiàn):
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version> </dependency>
2.2 創(chuàng)建令牌桶限流器
@Component public class RateLimiter { // 使用ConcurrentHashMap存儲不同接口的令牌桶 private final ConcurrentHashMap<String, com.google.common.util.concurrent.RateLimiter> rateLimiterMap = new ConcurrentHashMap<>(); /** * 獲取特定接口的令牌桶,不存在則創(chuàng)建 * @param key 限流鍵 * @param permitsPerSecond 每秒允許的請求量 * @return 令牌桶實例 */ public com.google.common.util.concurrent.RateLimiter getRateLimiter(String key, double permitsPerSecond) { return rateLimiterMap.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create(permitsPerSecond)); } /** * 嘗試獲取令牌 * @param key 限流鍵 * @param permitsPerSecond 每秒允許的請求量 * @param timeout 超時時間 * @param unit 時間單位 * @return 是否獲取成功 */ public boolean tryAcquire(String key, double permitsPerSecond, long timeout, TimeUnit unit) { com.google.common.util.concurrent.RateLimiter rateLimiter = getRateLimiter(key, permitsPerSecond); return rateLimiter.tryAcquire(1, timeout, unit); } }
2.3 創(chuàng)建攔截器
@Component public class TokenBucketInterceptor implements HandlerInterceptor { @Autowired private RateLimiter rateLimiter; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 僅對API請求進行限流 String requestURI = request.getRequestURI(); if (!requestURI.startsWith("/api/")) { return true; } // 獲取IP地址作為限流鍵 String ip = getIpAddress(request); String key = ip + ":" + requestURI; // 嘗試獲取令牌,設(shè)置每秒2個請求的速率,等待100毫秒 boolean acquired = rateLimiter.tryAcquire(key, 2.0, 100, TimeUnit.MILLISECONDS); if (!acquired) { // 獲取失敗,返回限流響應(yīng) response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); response.getWriter().write("{"code":429,"message":"請求過于頻繁,請稍后再試"}"); return false; } return true; } // getIpAddress方法同上 }
2.4 配置攔截器
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private TokenBucketInterceptor tokenBucketInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenBucketInterceptor) .addPathPatterns("/**"); } }
優(yōu)缺點分析
優(yōu)點:
- 支持突發(fā)流量,不會完全拒絕短時高峰
- 平滑的限流效果,用戶體驗更好
- 可以配置不同接口的不同限流策略
- 無需額外的存儲設(shè)施
缺點:
- 只適用于單機部署,分布式環(huán)境需要額外改造
- 重啟應(yīng)用后狀態(tài)丟失
- 無法精確控制時間窗口內(nèi)的請求總量
3. 分布式限流(Redis + Lua腳本)
對于分布式系統(tǒng),單機限流方案難以滿足需求。利用Redis和Lua腳本可以實現(xiàn)高效的分布式限流。
實現(xiàn)步驟
3.1 定義Lua腳本
創(chuàng)建一個Redis限流的Lua腳本,放在resources目錄下的scripts/rate_limiter.lua
:
-- 限流Key local key = KEYS[1] -- 限流窗口,單位秒 local window = tonumber(ARGV[1]) -- 限流閾值 local threshold = tonumber(ARGV[2]) -- 當前時間戳 local now = tonumber(ARGV[3]) -- 移除過期的請求記錄 redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000) -- 獲取當前窗口內(nèi)的請求數(shù) local count = redis.call('ZCARD', key) -- 如果請求數(shù)超過閾值,拒絕請求 if count >= threshold then return 0 end -- 添加當前請求記錄 redis.call('ZADD', key, now, now .. '-' .. math.random()) -- 設(shè)置過期時間 redis.call('EXPIRE', key, window) -- 返回當前窗口剩余可用請求數(shù) return threshold - count - 1
3.2 創(chuàng)建Redis限流服務(wù)
@Service @Slf4j public class RedisRateLimiterService { @Autowired private StringRedisTemplate redisTemplate; private DefaultRedisScript<Long> rateLimiterScript; @PostConstruct public void init() { // 加載Lua腳本 rateLimiterScript = new DefaultRedisScript<>(); rateLimiterScript.setLocation(new ClassPathResource("scripts/rate_limiter.lua")); rateLimiterScript.setResultType(Long.class); } /** * 嘗試獲取訪問權(quán)限 * @param key 限流鍵 * @param window 時間窗口(秒) * @param threshold 閾值 * @return 剩余可用請求數(shù),-1表示被限流 */ public long isAllowed(String key, int window, int threshold) { try { // 執(zhí)行l(wèi)ua腳本 List<String> keys = Collections.singletonList(key); Long remainingCount = redisTemplate.execute( rateLimiterScript, keys, String.valueOf(window), String.valueOf(threshold), String.valueOf(System.currentTimeMillis()) ); return remainingCount == null ? -1 : remainingCount; } catch (Exception e) { log.error("Redis rate limiter error", e); // 發(fā)生異常時放行請求 return threshold; } } }
3.3 創(chuàng)建分布式限流注解
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DistributedRateLimit { /** * 限流的key前綴 */ String prefix() default "rate:"; /** * 時間窗口,單位秒 */ int window() default 60; /** * 在時間窗口內(nèi)允許的最大請求數(shù) */ int threshold() default 10; /** * 限流模式: ip - 按IP限流, user - 按用戶限流, all - 接口總體限流 */ String mode() default "ip"; }
3.4 實現(xiàn)分布式限流切面
@Aspect @Component @Slf4j public class DistributedRateLimitAspect { @Autowired private RedisRateLimiterService rateLimiterService; @Autowired(required = false) private HttpServletRequest request; @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint pjp, DistributedRateLimit rateLimit) throws Throwable { String key = generateKey(pjp, rateLimit); long remainingCount = rateLimiterService.isAllowed( key, rateLimit.window(), rateLimit.threshold() ); if (remainingCount < 0) { throw new RuntimeException("接口訪問過于頻繁,請稍后再試"); } // 執(zhí)行目標方法 return pjp.proceed(); } private String generateKey(ProceedingJoinPoint pjp, DistributedRateLimit rateLimit) { String methodName = pjp.getSignature().getName(); String className = pjp.getTarget().getClass().getName(); StringBuilder key = new StringBuilder(rateLimit.prefix()); key.append(className).append(".").append(methodName); // 根據(jù)限流模式添加不同的后綴 switch (rateLimit.mode()) { case "ip": // 按IP限流 key.append(":").append(getIpAddress()); break; case "user": // 按用戶限流 Object userId = getUserId(); key.append(":").append(userId != null ? userId : "anonymous"); break; case "all": // 接口總體限流,不添加后綴 break; default: key.append(":").append(getIpAddress()); break; } return key.toString(); } private String getIpAddress() { // IP獲取方法同上 if (request == null) { return "unknown"; } // 獲取IP的代碼同上一個示例 return "127.0.0.1"; // 簡化處理 } // 獲取當前用戶ID,根據(jù)實際認證系統(tǒng)實現(xiàn) private Object getUserId() { // 這里簡化處理,實際中應(yīng)從認證信息中獲取 // 例如:SecurityContextHolder.getContext().getAuthentication().getPrincipal() return null; } }
3.5 使用示例
@RestController @RequestMapping("/api") public class PaymentController { @DistributedRateLimit(prefix = "pay:", window = 3600, threshold = 5, mode = "user") @PostMapping("/payment") public Result createPayment(@RequestBody PaymentRequest paymentRequest) { // 創(chuàng)建支付業(yè)務(wù)邏輯 return paymentService.createPayment(paymentRequest); } @DistributedRateLimit(window = 60, threshold = 30, mode = "ip") @GetMapping("/products") public List<Product> getProducts() { // 查詢產(chǎn)品列表 return productService.findAll(); } @DistributedRateLimit(window = 1, threshold = 100, mode = "all") @GetMapping("/hot/resource") public Resource getHotResource() { // 獲取熱門資源 return resourceService.getHotResource(); } }
優(yōu)缺點分析
優(yōu)點:
- 適用于分布式系統(tǒng),多實例間共享限流狀態(tài)
- 支持多種限流模式:按IP、用戶、接口總量等
- 基于滑動窗口,計數(shù)更精確
- 使用Lua腳本保證原子性,避免競態(tài)條件
缺點:
- 強依賴Redis
- 實現(xiàn)復(fù)雜度較高
4. 集成Sentinel實現(xiàn)接口防刷
阿里巴巴開源的Sentinel是一個強大的流量控制組件,提供了豐富的限流、熔斷、系統(tǒng)保護等功能。
實現(xiàn)步驟
4.1 添加依賴
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2021.0.4.0</version> </dependency>
4.2 配置Sentinel
在application.properties
中添加配置:
# Sentinel 控制臺地址 spring.cloud.sentinel.transport.dashboard=localhost:8080 # 取消Sentinel控制臺懶加載 spring.cloud.sentinel.eager=true # 應(yīng)用名稱 spring.application.name=my-application
4.3 創(chuàng)建Sentinel配置
@Configuration public class SentinelConfig { @Bean public SentinelResourceAspect sentinelResourceAspect() { return new SentinelResourceAspect(); } @PostConstruct public void init() { // 定義流控規(guī)則 initFlowRules(); } private void initFlowRules() { List<FlowRule> rules = new ArrayList<>(); // 為/api/user接口設(shè)置流控規(guī)則 FlowRule userRule = new FlowRule(); userRule.setResource("/api/user"); userRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 基于QPS限流 userRule.setCount(10); // 每秒允許10個請求 rules.add(userRule); // 為/api/order接口設(shè)置流控規(guī)則 FlowRule orderRule = new FlowRule(); orderRule.setResource("/api/order"); orderRule.setGrade(RuleConstant.FLOW_GRADE_QPS); orderRule.setCount(5); // 每秒允許5個請求 orderRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); // 預(yù)熱模式 orderRule.setWarmUpPeriodSec(10); // 10秒預(yù)熱期 rules.add(orderRule); // 加載規(guī)則 FlowRuleManager.loadRules(rules); } }
4.4 創(chuàng)建URL資源解析器
@Component public class UrlCleaner implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest request) { // 獲取請求的URL路徑 String path = request.getRequestURI(); // 可以添加更復(fù)雜的解析邏輯,例如: // 1. 去除路徑變量:/api/user/123 -> /api/user/{id} // 2. 添加請求方法前綴:GET:/api/user return path; } }
4.5 創(chuàng)建全局異常處理器
@RestControllerAdvice public class SentinelExceptionHandler { @ExceptionHandler(BlockException.class) public Result handleBlockException(BlockException e) { String message = "請求過于頻繁,請稍后再試"; if (e instanceof FlowException) { message = "接口限流:" + message; } else if (e instanceof DegradeException) { message = "服務(wù)降級:系統(tǒng)繁忙,請稍后再試"; } else if (e instanceof ParamFlowException) { message = "熱點參數(shù)限流:請求過于頻繁"; } else if (e instanceof SystemBlockException) { message = "系統(tǒng)保護:系統(tǒng)資源不足"; } else if (e instanceof AuthorityException) { message = "授權(quán)控制:沒有訪問權(quán)限"; } return Result.error(429, message); } }
4.6 使用@SentinelResource注解
@RestController @RequestMapping("/api") public class UserController { // 使用資源名定義限流資源 @SentinelResource(value = "getUserById", blockHandler = "getUserBlockHandler", fallback = "getUserFallback") @GetMapping("/user/{id}") public User getUser(@PathVariable Long id) { return userService.getUser(id); } // 限流處理方法 public User getUserBlockHandler(Long id, BlockException e) { log.warn("Get user request blocked: {}", id, e); throw new RuntimeException("請求頻率過高,請稍后再試"); } // 異?;赝朔椒? public User getUserFallback(Long id, Throwable t) { log.error("Get user failed: {}", id, t); User fallbackUser = new User(); fallbackUser.setId(id); fallbackUser.setName("Unknown"); return fallbackUser; } }
4.7 更復(fù)雜的限流規(guī)則配置
@Service @Slf4j public class SentinelRuleService { public void initComplexFlowRules() { List<FlowRule> rules = new ArrayList<>(); // 基于QPS + 調(diào)用關(guān)系的限流規(guī)則 FlowRule apiRule = new FlowRule(); apiRule.setResource("/api/data"); apiRule.setGrade(RuleConstant.FLOW_GRADE_QPS); apiRule.setCount(20); // 限制調(diào)用來源 apiRule.setLimitApp("frontend"); // 只限制來自前端應(yīng)用的調(diào)用 // 流控策略:關(guān)聯(lián)資源 apiRule.setStrategy(RuleConstant.STRATEGY_RELATE); apiRule.setRefResource("/api/important"); // 當important接口QPS高時,限制data接口 rules.add(apiRule); // 基于并發(fā)線程數(shù)的限流 FlowRule threadRule = new FlowRule(); threadRule.setResource("/api/heavy-task"); threadRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); // 基于線程數(shù) threadRule.setCount(5); // 最多5個線程同時處理 rules.add(threadRule); // 加載規(guī)則 FlowRuleManager.loadRules(rules); } public void initHotspotRules() { // 熱點參數(shù)限流規(guī)則 List<ParamFlowRule> rules = new ArrayList<>(); ParamFlowRule rule = new ParamFlowRule("/api/product"); // 對第0個參數(shù)(productId)進行限流 rule.setParamIdx(0); rule.setCount(5); // 特例配置 ParamFlowItem item1 = new ParamFlowItem(); item1.setObject("1"); // productId = 1的商品 item1.setCount(10); // 可以有更高的QPS ParamFlowItem item2 = new ParamFlowItem(); item2.setObject("2"); // productId = 2的商品 item2.setCount(2); // 更嚴格的限制 rule.setParamFlowItemList(Arrays.asList(item1, item2)); rules.add(rule); ParamFlowRuleManager.loadRules(rules); } }
優(yōu)缺點分析
優(yōu)點:
- 功能全面,支持QPS限流、并發(fā)線程數(shù)限流、熱點參數(shù)限流等
- 支持多種控制策略:直接拒絕、預(yù)熱、排隊等
- 提供控制臺可視化管理
- 支持動態(tài)規(guī)則調(diào)整
- 可與Spring Cloud體系無縫集成
缺點:
- 學(xué)習(xí)曲線較陡峭
- 分布式場景下需要額外配置規(guī)則持久化
- 引入了額外的依賴
5. 驗證碼與行為分析防刷
對于某些敏感操作(如登錄、注冊、支付等),可以結(jié)合驗證碼和行為分析來防止惡意請求。
實現(xiàn)步驟
5.1 圖形驗證碼實現(xiàn)
首先添加依賴:
<dependency> <groupId>com.github.whvcse</groupId> <artifactId>easy-captcha</artifactId> <version>1.6.2</version> </dependency>
5.2 創(chuàng)建驗證碼服務(wù)
@Service public class CaptchaService { @Autowired private StringRedisTemplate redisTemplate; private static final long CAPTCHA_EXPIRE_TIME = 5 * 60; // 5分鐘 /** * 生成驗證碼 * @param request HTTP請求 * @param response HTTP響應(yīng) * @return 驗證碼Base64字符串 */ public String generateCaptcha(HttpServletRequest request, HttpServletResponse response) { // 生成驗證碼 SpecCaptcha captcha = new SpecCaptcha(130, 48, 5); // 生成驗證碼ID String captchaId = UUID.randomUUID().toString(); // 將驗證碼存入Redis redisTemplate.opsForValue().set( "captcha:" + captchaId, captcha.text().toLowerCase(), CAPTCHA_EXPIRE_TIME, TimeUnit.SECONDS ); // 設(shè)置Cookie Cookie cookie = new Cookie("captchaId", captchaId); cookie.setMaxAge((int) CAPTCHA_EXPIRE_TIME); cookie.setPath("/"); response.addCookie(cookie); // 返回Base64編碼的驗證碼圖片 return captcha.toBase64(); } /** * 驗證驗證碼 * @param request HTTP請求 * @param captchaCode 用戶輸入的驗證碼 * @return 是否驗證通過 */ public boolean validateCaptcha(HttpServletRequest request, String captchaCode) { // 從Cookie獲取驗證碼ID Cookie[] cookies = request.getCookies(); String captchaId = null; if (cookies != null) { for (Cookie cookie : cookies) { if ("captchaId".equals(cookie.getName())) { captchaId = cookie.getValue(); break; } } } if (captchaId == null) { return false; } // 從Redis獲取正確的驗證碼 String key = "captcha:" + captchaId; String correctCode = redisTemplate.opsForValue().get(key); // 驗證成功后刪除驗證碼 if (correctCode != null && correctCode.equals(captchaCode.toLowerCase())) { redisTemplate.delete(key); return true; } return false; } }
5.3 創(chuàng)建驗證碼控制器
@RestController @RequestMapping("/api/captcha") public class CaptchaController { @Autowired private CaptchaService captchaService; @GetMapping public Map<String, String> getCaptcha(HttpServletRequest request, HttpServletResponse response) { String base64 = captchaService.generateCaptcha(request, response); return Map.of("captcha", base64); } }
5.4 創(chuàng)建驗證碼注解
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CaptchaRequired { String captchaParam() default "captchaCode"; }
5.5 實現(xiàn)驗證碼攔截器
@Component public class CaptchaInterceptor implements HandlerInterceptor { @Autowired private CaptchaService captchaService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; CaptchaRequired captchaRequired = handlerMethod.getMethodAnnotation(CaptchaRequired.class); if (captchaRequired == null) { return true; } // 獲取驗證碼參數(shù) String captchaParam = captchaRequired.captchaParam(); String captchaCode = request.getParameter(captchaParam); if (StringUtils.hasText(captchaCode)) { // 驗證驗證碼 boolean valid = captchaService.validateCaptcha(request, captchaCode); if (valid) { return true; } } // 驗證失敗 response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.BAD_REQUEST.value()); response.getWriter().write("{"code":400,"message":"驗證碼錯誤或已過期"}"); return false; } }
5.6 創(chuàng)建行為分析服務(wù)
@Service @Slf4j public class BehaviorAnalysisService { @Autowired private StringRedisTemplate redisTemplate; /** * 檢查是否是可疑的機器行為 * @param request HTTP請求 * @return 是否可疑 */ public boolean isSuspicious(HttpServletRequest request) { // 1. 獲取客戶端信息 String ip = getIpAddress(request); String userAgent = request.getHeader("User-Agent"); String requestId = request.getSession().getId(); // 2. 檢查訪問頻率 String freqKey = "behavior:freq:" + ip; Long count = redisTemplate.opsForValue().increment(freqKey, 1); redisTemplate.expire(freqKey, 1, TimeUnit.MINUTES); if (count != null && count > 30) { log.warn("訪問頻率異常: IP={}, count={}", ip, count); return true; } // 3. 檢查User-Agent if (userAgent == null || isBotuserAgent(userAgent)) { log.warn("可疑的User-Agent: {}", userAgent); return true; } // 4. 檢查請求時間模式 String timeKey = "behavior:time:" + ip; long now = System.currentTimeMillis(); String lastTimeStr = redisTemplate.opsForValue().get(timeKey); if (lastTimeStr != null) { long lastTime = Long.parseLong(lastTimeStr); long interval = now - lastTime; // 如果請求間隔非常均勻,可能是機器人 if (isUniformInterval(ip, interval)) { log.warn("請求間隔異常均勻: IP={}, interval={}", ip, interval); return true; } } redisTemplate.opsForValue().set(timeKey, String.valueOf(now), 10, TimeUnit.MINUTES); // 更多高級檢測邏輯... return false; } /** * 檢查是否是機器人UA */ private boolean isBotuserAgent(String userAgent) { String ua = userAgent.toLowerCase(); return ua.contains("bot") || ua.contains("spider") || ua.contains("crawl") || ua.isEmpty() || ua.length() < 40; } /** * 檢查請求間隔是否異常均勻 */ private boolean isUniformInterval(String ip, long interval) { String key = "behavior:intervals:" + ip; // 獲取最近的幾個間隔 List<String> intervalStrs = redisTemplate.opsForList().range(key, 0, 4); redisTemplate.opsForList().leftPush(key, String.valueOf(interval)); redisTemplate.opsForList().trim(key, 0, 9); // 只保留最近10個 redisTemplate.expire(key, 10, TimeUnit.MINUTES); if (intervalStrs == null || intervalStrs.size() < 5) { return false; } // 計算間隔的方差,方差小說明請求間隔很均勻 List<Long> intervals = intervalStrs.stream() .map(Long::parseLong) .collect(Collectors.toList()); double mean = intervals.stream().mapToLong(Long::longValue).average().orElse(0); double variance = intervals.stream() .mapToDouble(i -> Math.pow(i - mean, 2)) .average() .orElse(0); return variance < 100; // 方差閾值,需要根據(jù)實際情況調(diào)整 } // getIpAddress方法同上 }
5.7 創(chuàng)建行為分析攔截器
@Component public class BehaviorAnalysisInterceptor implements HandlerInterceptor { @Autowired private BehaviorAnalysisService behaviorAnalysisService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 對于需要保護的端點進行檢查 String path = request.getRequestURI(); if (path.startsWith("/api/") && isPotentialRiskEndpoint(path)) { boolean suspicious = behaviorAnalysisService.isSuspicious(request); if (suspicious) { // 需要驗證碼或其他額外驗證 response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); response.getWriter().write("{"code":429,"message":"檢測到異常訪問,請進行驗證","needCaptcha":true}"); return false; } } return true; } /** * 判斷是否是高風(fēng)險端點 */ private boolean isPotentialRiskEndpoint(String path) { return path.contains("/login") || path.contains("/register") || path.contains("/payment") || path.contains("/order") || path.contains("/password"); } }
5.8 使用示例
@RestController @RequestMapping("/api") public class UserController { @CaptchaRequired @PostMapping("/login") public Result login(@RequestParam String username, @RequestParam String password, @RequestParam String captchaCode) { // 登錄邏輯 return userService.login(username, password); } @CaptchaRequired @PostMapping("/register") public Result register(@RequestBody UserRegisterDTO registerDTO, @RequestParam String captchaCode) { // 注冊邏輯 return userService.register(registerDTO); } }
優(yōu)缺點分析
優(yōu)點:
- 能有效區(qū)分人類用戶和自動化腳本
- 對惡意用戶有較強的阻止作用
- 針對敏感操作提供額外安全層
- 可以實現(xiàn)自適應(yīng)安全策略
缺點:
- 增加了用戶操作成本,可能影響用戶體驗
- 實現(xiàn)復(fù)雜,需要前后端配合
- 某些驗證碼可能被OCR技術(shù)破解
- 行為分析可能產(chǎn)生誤判
方案對比與選擇
方案 | 實現(xiàn)難度 | 防刷效果 | 分布式支持 | 用戶體驗 | 適用場景 |
---|---|---|---|---|---|
基于注解的訪問頻率限制 | 低 | 中 | 需配合Redis | 一般 | 一般接口,簡單場景 |
令牌桶算法 | 中 | 中高 | 單機 | 好 | 允許突發(fā)流量的場景 |
分布式限流(Redis+Lua) | 高 | 高 | 支持 | 一般 | 分布式系統(tǒng),精確限流 |
Sentinel | 中高 | 高 | 需額外配置 | 可配置 | 復(fù)雜系統(tǒng),多維度防護 |
驗證碼與行為分析 | 高 | 高 | 支持 | 較差 | 敏感操作,關(guān)鍵業(yè)務(wù) |
總結(jié)
接口防刷是一個系統(tǒng)性工程,需要考慮多方面因素:安全性、用戶體驗、性能開銷和運維復(fù)雜度等。本文介紹的5種方案各有優(yōu)缺點,可以根據(jù)實際需求靈活選擇和組合。
無論采用哪種方案,接口防刷都應(yīng)該遵循以下原則:
- 最小影響原則:盡量不影響正常用戶的體驗
- 梯度防護原則:根據(jù)接口的重要程度采用不同強度的防護措施
- 可監(jiān)控原則:提供充分的監(jiān)控和告警機制
- 靈活調(diào)整原則:支持動態(tài)調(diào)整防護參數(shù)和策略
通過合理實施接口防刷策略,可以有效提高系統(tǒng)的安全性和穩(wěn)定性,為用戶提供更好的服務(wù)體驗。
以上就是SpringBoot實現(xiàn)接口防刷的五種方案的詳細內(nèi)容,更多關(guān)于SpringBoot接口防刷的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java利用Redis實現(xiàn)高并發(fā)計數(shù)器的示例代碼
這篇文章主要介紹了Java利用Redis實現(xiàn)高并發(fā)計數(shù)器的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02Java中final作用于變量、參數(shù)、方法及類該如何處理
Java中的final關(guān)鍵字非常重要,它可以應(yīng)用于類、方法以及變量,下面這篇文章主要給大家介紹了關(guān)于Java中final作用于變量、參數(shù)、方法及類該如何處理的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下。2017-12-12SpringBoot導(dǎo)出Excel的四種方式小結(jié)
近期接到了一個小需求,要將系統(tǒng)中的數(shù)據(jù)導(dǎo)出為Excel,且能將Excel數(shù)據(jù)導(dǎo)入到系統(tǒng),對于大多數(shù)研發(fā)人員來說,這算是一個最基本的操作了,本文就給大家總結(jié)一下SpringBoot導(dǎo)出Excel的四種實現(xiàn)方式,需要的朋友可以參考下2024-12-12SpringCloud微服務(wù)架構(gòu)實戰(zhàn)之微服務(wù)治理功能的實現(xiàn)
這篇文章主要介紹了SpringCloud微服務(wù)架構(gòu)實戰(zhàn)之微服務(wù)治理,這些治理工具主要包括服務(wù)的注冊與發(fā)現(xiàn)、負載均衡管理、動態(tài)路由、服務(wù)降級和故障轉(zhuǎn)移、鏈路跟蹤、服務(wù)監(jiān)控等,需要的朋友可以參考下2022-02-02jsp頁面中獲取servlet請求中的參數(shù)的辦法詳解
在JAVA WEB應(yīng)用中,如何獲取servlet請求中的參數(shù),本文講解了jsp頁面中獲取servlet請求中的參數(shù)的辦法2018-03-03