目录
1. 基于注解的访问频率限制
实现步骤
1.1 创建限流注解
1.2 实现限流切面
1.3 使用示例
优缺点分析
优点:
缺点:
2. 令牌桶算法实现限流
实现步骤
2.1 引入依赖
2.2 创建令牌桶限流器
2.3 创建拦截器
2.4 配置拦截器
优缺点分析
优点:
缺点:
3. 分布式限流(Redis + Lua脚本)
实现步骤
3.1 定义Lua脚本
3.2 创建Redis限流服务
3.3 创建分布式限流注解
3.4 实现分布式限流切面
3.5 使用示例
优缺点分析
优点:
缺点:
4. 集成Sentinel实现接口防刷
实现步骤
4.1 添加依赖
4.2 配置Sentinel
4.3 创建Sentinel配置
4.4 创建URL资源解析器
4.5 创建全局异常处理器
4.6 使用@SentinelResource注解
4.7 更复杂的限流规则配置
优缺点分析
优点:
缺点:
5. 验证码与行为分析防刷
实现步骤
5.1 图形验证码实现
5.2 创建验证码服务
5.3 创建验证码控制器
5.4 创建验证码注解
5.5 实现验证码拦截器
5.6 创建行为分析服务
5.7 创建行为分析拦截器
5.8 使用示例
优缺点分析
优点:
缺点:
6.方案对比与选择
7.总结
接口防刷是保障系统安全与稳定性的重要措施。恶意的高频请求不仅会消耗服务器资源,还可能导致数据异常,甚至系统瘫痪。
本文将介绍在SpringBoot框架下实现接口防刷的5种技术方案。
1. 基于注解的访问频率限制
最常见的防刷方案是通过自定义注解和AOP切面实现访问频率限制。这种方法简单易用,实现成本低。
实现步骤
1.1 创建限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {/*** 限制时间段,单位为秒*/int time() default 60;/*** 在限制时间段内允许的最大请求次数*/int count() default 10;/*** 限流的key,支持SpEL表达式*/String key() default "";/*** 提示信息*/String message() default "操作太频繁,请稍后再试";
}
1.2 实现限流切面
@Aspect
@Component
@Slf4j
public class RateLimitAspect {@Autowiredprivate 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();// 组合限流keyString limitKey = getLimitKey(pjp, rateLimit, methodName, className);// 获取限流参数int time = rateLimit.time();int count = rateLimit.count();// 执行限流逻辑boolean limited = isLimited(limitKey, time, count);if (limited) {throw new RuntimeException(rateLimit.message());}// 执行目标方法return pjp.proceed();}private String getLimitKey(ProceedingJoinPoint pjp, RateLimit rateLimit, String methodName, String className) {// 获取用户自定义的keyString 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地址作为keyHttpServletRequest 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的计数器实现限流try {Long currentCount = redisTemplate.opsForValue().increment(key, 1);// 如果是第一次访问,设置过期时间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);}
}
优缺点分析
优点:
-
• 实现简单,上手容易,单机情况下可以去掉Redis换成本地缓存实现
-
• 注解式使用,对业务代码无侵入
-
• 可以精确控制接口粒度
-
• 支持灵活的限流策略配置
缺点:
-
• 限流逻辑相对简单,无法应对复杂场景
-
• 缺少预警机制
2. 令牌桶算法实现限流
令牌桶算法是一种更加灵活的限流算法,可以允许突发流量,同时又能限制长期的平均流量。
实现步骤
2.1 引入依赖
Google提供的Guava库中包含了令牌桶实现:
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>31.1-jre</version>
</dependency>
2.2 创建令牌桶限流器
@Component
public class RateLimiter {// 使用ConcurrentHashMap存储不同接口的令牌桶private final ConcurrentHashMap<String, com.google.common.util.concurrent.RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();/*** 获取特定接口的令牌桶,不存在则创建* @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 创建拦截器
@Component
public class TokenBucketInterceptor implements HandlerInterceptor {@Autowiredprivate RateLimiter rateLimiter;@Overridepublic 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;// 尝试获取令牌,设置每秒2个请求的速率,等待100毫秒boolean acquired = rateLimiter.tryAcquire(key, 2.0, 100, TimeUnit.MILLISECONDS);if (!acquired) {// 获取失败,返回限流响应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 {@Autowiredprivate TokenBucketInterceptor tokenBucketInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(tokenBucketInterceptor).addPathPatterns("/**");}
}
优缺点分析
优点:
-
• 支持突发流量,不会完全拒绝短时高峰
-
• 平滑的限流效果,用户体验更好
-
• 可以配置不同接口的不同限流策略
-
• 无需额外的存储设施
缺点:
-
• 只适用于单机部署,分布式环境需要额外改造
-
• 重启应用后状态丢失
-
• 无法精确控制时间窗口内的请求总量
3. 分布式限流(Redis + Lua脚本)
对于分布式系统,单机限流方案难以满足需求。利用Redis和Lua脚本可以实现高效的分布式限流。
实现步骤
3.1 定义Lua脚本
创建一个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)-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)-- 如果请求数超过阈值,拒绝请求
if count >= threshold thenreturn 0
end-- 添加当前请求记录
redis.call('ZADD', key, now, now .. '-' .. math.random())
-- 设置过期时间
redis.call('EXPIRE', key, window)-- 返回当前窗口剩余可用请求数
return threshold - count - 1
3.2 创建Redis限流服务
@Service
@Slf4j
public class RedisRateLimiterService {@Autowiredprivate StringRedisTemplate redisTemplate;private DefaultRedisScript<Long> rateLimiterScript;@PostConstructpublic void init() {// 加载Lua脚本rateLimiterScript = new DefaultRedisScript<>();rateLimiterScript.setLocation(new ClassPathResource("scripts/rate_limiter.lua"));rateLimiterScript.setResultType(Long.class);}/*** 尝试获取访问权限* @param key 限流键* @param window 时间窗口(秒)* @param threshold 阈值* @return 剩余可用请求数,-1表示被限流*/public long isAllowed(String key, int window, int threshold) {try {// 执行lua脚本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);// 发生异常时放行请求return threshold;}}
}
3.3 创建分布式限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedRateLimit {/*** 限流的key前缀*/String prefix() default "rate:";/*** 时间窗口,单位秒*/int window() default 60;/*** 在时间窗口内允许的最大请求数*/int threshold() default 10;/*** 限流模式: ip - 按IP限流, user - 按用户限流, all - 接口总体限流*/String mode() default "ip";
}
3.4 实现分布式限流切面
@Aspect
@Component
@Slf4j
public class DistributedRateLimitAspect {@Autowiredprivate 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("接口访问过于频繁,请稍后再试");}// 执行目标方法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);// 根据限流模式添加不同的后缀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,根据实际认证系统实现private Object getUserId() {// 这里简化处理,实际中应从认证信息中获取// 例如: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) {// 创建支付业务逻辑return paymentService.createPayment(paymentRequest);}@DistributedRateLimit(window = 60, threshold = 30, mode = "ip")@GetMapping("/products")public List<Product> getProducts() {// 查询产品列表return productService.findAll();}@DistributedRateLimit(window = 1, threshold = 100, mode = "all")@GetMapping("/hot/resource")public Resource getHotResource() {// 获取热门资源return resourceService.getHotResource();}
}
优缺点分析
优点:
-
• 适用于分布式系统,多实例间共享限流状态
-
• 支持多种限流模式:按IP、用户、接口总量等
-
• 基于滑动窗口,计数更精确
-
• 使用Lua脚本保证原子性,避免竞态条件
缺点:
-
• 强依赖Redis
-
• 实现复杂度较高
4. 集成Sentinel实现接口防刷
阿里巴巴开源的Sentinel是一个强大的流量控制组件,提供了丰富的限流、熔断、系统保护等功能。
实现步骤
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
# 应用名称
spring.application.name=my-application
4.3 创建Sentinel配置
@Configuration
public class SentinelConfig {@Beanpublic SentinelResourceAspect sentinelResourceAspect() {return new SentinelResourceAspect();}@PostConstructpublic void init() {// 定义流控规则initFlowRules();}private void initFlowRules() {List<FlowRule> rules = new ArrayList<>();// 为/api/user接口设置流控规则FlowRule userRule = new FlowRule();userRule.setResource("/api/user");userRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 基于QPS限流userRule.setCount(10); // 每秒允许10个请求rules.add(userRule);// 为/api/order接口设置流控规则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); // 预热模式orderRule.setWarmUpPeriodSec(10); // 10秒预热期rules.add(orderRule);// 加载规则FlowRuleManager.loadRules(rules);}
}
4.4 创建URL资源解析器
@Component
public class UrlCleaner implements RequestOriginParser {@Overridepublic String parseOrigin(HttpServletRequest request) {// 获取请求的URL路径String path = request.getRequestURI();// 可以添加更复杂的解析逻辑,例如:// 1. 去除路径变量:/api/user/123 -> /api/user/{id}// 2. 添加请求方法前缀:GET:/api/userreturn path;}
}
4.5 创建全局异常处理器
@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 = "服务降级:系统繁忙,请稍后再试";} else if (e instanceof ParamFlowException) {message = "热点参数限流:请求过于频繁";} else if (e instanceof SystemBlockException) {message = "系统保护:系统资源不足";} else if (e instanceof AuthorityException) {message = "授权控制:没有访问权限";}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 更复杂的限流规则配置
@Service
@Slf4j
public class SentinelRuleService {public void initComplexFlowRules() {List<FlowRule> rules = new ArrayList<>();// 基于QPS + 调用关系的限流规则FlowRule apiRule = new FlowRule();apiRule.setResource("/api/data");apiRule.setGrade(RuleConstant.FLOW_GRADE_QPS);apiRule.setCount(20);// 限制调用来源apiRule.setLimitApp("frontend"); // 只限制来自前端应用的调用// 流控策略:关联资源apiRule.setStrategy(RuleConstant.STRATEGY_RELATE);apiRule.setRefResource("/api/important"); // 当important接口QPS高时,限制data接口rules.add(apiRule);// 基于并发线程数的限流FlowRule threadRule = new FlowRule();threadRule.setResource("/api/heavy-task");threadRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); // 基于线程数threadRule.setCount(5); // 最多5个线程同时处理rules.add(threadRule);// 加载规则FlowRuleManager.loadRules(rules);}public void initHotspotRules() {// 热点参数限流规则List<ParamFlowRule> rules = new ArrayList<>();ParamFlowRule rule = new ParamFlowRule("/api/product");// 对第0个参数(productId)进行限流rule.setParamIdx(0);rule.setCount(5);// 特例配置ParamFlowItem item1 = new ParamFlowItem();item1.setObject("1"); // productId = 1的商品item1.setCount(10); // 可以有更高的QPSParamFlowItem item2 = new ParamFlowItem();item2.setObject("2"); // productId = 2的商品item2.setCount(2); // 更严格的限制rule.setParamFlowItemList(Arrays.asList(item1, item2));rules.add(rule);ParamFlowRuleManager.loadRules(rules);}
}
优缺点分析
优点:
-
• 功能全面,支持QPS限流、并发线程数限流、热点参数限流等
-
• 支持多种控制策略:直接拒绝、预热、排队等
-
• 提供控制台可视化管理
-
• 支持动态规则调整
-
• 可与Spring Cloud体系无缝集成
缺点:
-
• 学习曲线较陡峭
-
• 分布式场景下需要额外配置规则持久化
-
• 引入了额外的依赖
5. 验证码与行为分析防刷
对于某些敏感操作(如登录、注册、支付等),可以结合验证码和行为分析来防止恶意请求。
实现步骤
5.1 图形验证码实现
首先添加依赖:
<dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version>
</dependency>
5.2 创建验证码服务
@Service
public class CaptchaService {@Autowiredprivate StringRedisTemplate redisTemplate;private static final long CAPTCHA_EXPIRE_TIME = 5 * 60; // 5分钟/*** 生成验证码* @param request HTTP请求* @param response HTTP响应* @return 验证码Base64字符串*/public String generateCaptcha(HttpServletRequest request, HttpServletResponse response) {// 生成验证码SpecCaptcha captcha = new SpecCaptcha(130, 48, 5);// 生成验证码IDString captchaId = UUID.randomUUID().toString();// 将验证码存入RedisredisTemplate.opsForValue().set("captcha:" + captchaId, captcha.text().toLowerCase(), CAPTCHA_EXPIRE_TIME, TimeUnit.SECONDS);// 设置CookieCookie 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获取验证码IDCookie[] 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 创建验证码控制器
@RestController
@RequestMapping("/api/captcha")
public class CaptchaController {@Autowiredprivate CaptchaService captchaService;@GetMappingpublic Map<String, String> getCaptcha(HttpServletRequest request, HttpServletResponse response) {String base64 = captchaService.generateCaptcha(request, response);return Map.of("captcha", base64);}
}
5.4 创建验证码注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaptchaRequired {String captchaParam() default "captchaCode";
}
5.5 实现验证码拦截器
@Component
public class CaptchaInterceptor implements HandlerInterceptor {@Autowiredprivate CaptchaService captchaService;@Overridepublic 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;}// 获取验证码参数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 创建行为分析服务
@Service
@Slf4j
public class BehaviorAnalysisService {@Autowiredprivate 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-Agentif (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; // 方差阈值,需要根据实际情况调整}// getIpAddress方法同上
}
5.7 创建行为分析拦截器
@Component
public class BehaviorAnalysisInterceptor implements HandlerInterceptor {@Autowiredprivate BehaviorAnalysisService behaviorAnalysisService;@Overridepublic 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;}/*** 判断是否是高风险端点*/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);}
}
优缺点分析
优点:
-
• 能有效区分人类用户和自动化脚本
-
• 对恶意用户有较强的阻止作用
-
• 针对敏感操作提供额外安全层
-
• 可以实现自适应安全策略
缺点:
-
• 增加了用户操作成本,可能影响用户体验
-
• 实现复杂,需要前后端配合
-
• 某些验证码可能被OCR技术破解
-
• 行为分析可能产生误判
6.方案对比与选择
方案 | 实现难度 | 防刷效果 | 分布式支持 | 用户体验 | 适用场景 |
基于注解的访问频率限制 | 低 | 中 | 需配合Redis | 一般 | 一般接口,简单场景 |
令牌桶算法 | 中 | 中高 | 单机 | 好 | 允许突发流量的场景 |
分布式限流(Redis+Lua) | 高 | 高 | 支持 | 一般 | 分布式系统,精确限流 |
Sentinel | 中高 | 高 | 需额外配置 | 可配置 | 复杂系统,多维度防护 |
验证码与行为分析 | 高 | 高 | 支持 | 较差 | 敏感操作,关键业务 |
7.总结
接口防刷是一个系统性工程,需要考虑多方面因素:安全性、用户体验、性能开销和运维复杂度等。本文介绍的5种方案各有优缺点,可以根据实际需求灵活选择和组合。
无论采用哪种方案,接口防刷都应该遵循以下原则:
-
• 最小影响原则:尽量不影响正常用户的体验
-
• 梯度防护原则:根据接口的重要程度采用不同强度的防护措施
-
• 可监控原则:提供充分的监控和告警机制
-
• 灵活调整原则:支持动态调整防护参数和策略
通过合理实施接口防刷策略,可以有效提高系统的安全性和稳定性,为用户提供更好的服务体验。
·······END·······
喜欢的话可以点个赞关注博主哦!!!