⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 码猿技术专栏 「码猿技术专栏」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

在当今的互联网环境中,接口防刷已成为保障系统安全与稳定运行的关键环节。恶意的高频请求如同隐藏在暗处的“杀手”,不仅会大量消耗服务器宝贵的资源,还可能引发数据异常,严重时甚至会导致整个系统瘫痪,给业务带来不可估量的损失。

本文将深入剖析在 Spring Boot 框架下实现接口防刷的 5 种技术方案,帮助你根据实际需求选择最合适的方法,为系统安全保驾护航。

1. 基于注解的访问频率限制

基于注解的访问频率限制是一种广受欢迎的接口防刷方案,它通过自定义注解和 AOP 切面的巧妙结合,实现了对接口访问频率的有效控制。这种方法简单易用,实现成本低,就像给接口穿上了一层轻便的“防护衣”。

实现步骤

1.1 创建限流注解

首先,我们需要创建一个自定义的限流注解,用于标记需要进行访问频率限制的接口方法。以下是具体的代码实现:

在这个注解中,我们可以通过 time 属性设置限制的时间段,通过 count 属性设置在该时间段内允许的最大请求次数,通过 key 属性指定限流的键,支持使用 SpEL 表达式进行灵活配置,message 属性则用于在请求被限定时给用户提供提示信息。

1.2 实现限流切面

接下来,我们需要实现一个 AOP 切面,用于处理限流逻辑。以下是具体的代码实现:

@Aspect
@Component
@Slf4j
publicclass 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);

// 获取限流参数
int time = rateLimit.time();
int count = rateLimit.count();

// 执行限流逻辑
boolean limited = isLimited(limitKey, time, count);
if (limited) {
thrownew RuntimeException(rateLimit.message());
}

// 执行目标方法
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 的计数器实现限流
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);
returnfalse;
}
}

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;
}
}

在这个切面中,我们使用 @Around 注解拦截所有标记了 @RateLimit 注解的方法。在方法执行前,我们会根据注解中的参数计算出限流的键,并使用 Redis 的计数器来记录请求次数。如果请求次数超过了限制,我们会抛出一个异常,提示用户操作过于频繁。

1.3 使用示例

以下是一个使用 @RateLimit 注解的示例:

在这个示例中,我们在 getUser 方法上使用了 @RateLimit 注解,设置了 60 秒内最多允许 3 次请求。在 updateUser 方法上,我们使用了 SpEL 表达式来动态生成限流的键,实现了更加灵活的限流策略。

优缺点分析

优点

  • 实现简单,上手容易:该方案的实现逻辑清晰,代码量较少,即使是初学者也能快速掌握。在单机情况下,甚至可以去掉 Redis 换成本地缓存实现,进一步降低了实现难度。
  • 注解式使用,对业务代码无侵入:通过自定义注解的方式,我们可以将限流逻辑与业务逻辑分离,对业务代码的改动非常小,不会影响原有的业务功能。
  • 可以精确控制接口粒度:我们可以针对不同的接口方法设置不同的限流参数,实现对接口访问频率的精确控制。
  • 支持灵活的限流策略配置:通过 SpEL 表达式,我们可以根据请求的参数、用户信息等动态生成限流的键,实现更加灵活的限流策略。

缺点

  • 限流逻辑相对简单,无法应对复杂场景:该方案的限流逻辑主要基于固定的时间窗口和请求次数,对于一些复杂的场景,如突发流量、动态限流等,可能无法满足需求。
  • 缺少预警机制:当请求次数接近或达到限制时,系统无法及时发出预警,可能会导致用户体验下降。

2. 令牌桶算法实现限流

令牌桶算法是一种更加灵活的限流算法,它就像一个装有令牌的桶,系统会以固定的速率向桶中添加令牌,每个请求需要从桶中获取一个令牌才能被处理。这种算法可以允许突发流量,同时又能限制长期的平均流量,为系统提供了更加灵活的流量控制能力。

实现步骤

2.1 引入依赖

Google 提供的 Guava 库中包含了令牌桶的实现,我们可以通过以下依赖将其引入项目:

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>

2.2 创建令牌桶限流器

接下来,我们需要创建一个令牌桶限流器,用于管理不同接口的令牌桶。以下是具体的代码实现:

@Component
publicclass RateLimiter {
// 使用 ConcurrentHashMap 存储不同接口的令牌桶
privatefinal 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);
}
}

在这个限流器中,我们使用 ConcurrentHashMap 来存储不同接口的令牌桶,确保线程安全。通过 getRateLimiter 方法,我们可以根据限流键获取对应的令牌桶,如果令牌桶不存在,则会自动创建一个新的令牌桶。通过 tryAcquire 方法,我们可以尝试从令牌桶中获取一个令牌,如果在指定的超时时间内获取成功,则返回 true,否则返回 false。

2.3 创建拦截器

为了实现对接口的限流,我们需要创建一个拦截器,在请求进入接口之前进行令牌的获取操作。以下是具体的代码实现:

在这个拦截器中,我们首先判断请求的 URI 是否以 /api/ 开头,如果是,则进行限流处理。然后,我们获取请求的 IP 地址和 URI,组合成限流键。接着,我们尝试从令牌桶中获取一个令牌,如果获取失败,则返回一个限流响应,提示用户请求过于频繁。

2.4 配置拦截器

最后,我们需要将拦截器配置到 Spring Boot 应用中,使其生效。以下是具体的代码实现:

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private TokenBucketInterceptor tokenBucketInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenBucketInterceptor)
.addPathPatterns("/**");
}
}

在这个配置类中,我们使用 WebMvcConfigurer 接口的 addInterceptors 方法将拦截器添加到拦截器链中,并设置拦截所有的请求。

优缺点分析

优点

  • 支持突发流量,不会完全拒绝短时高峰:令牌桶算法允许在短时间内有较高的请求速率,只要桶中有足够的令牌。当突发流量到来时,系统可以快速处理这些请求,而不会像固定窗口算法那样直接拒绝请求。
  • 平滑的限流效果,用户体验更好:由于令牌桶算法是以固定的速率向桶中添加令牌,因此可以实现平滑的限流效果,避免了请求的突然中断,提高了用户体验。
  • 可以配置不同接口的不同限流策略:通过使用不同的限流键,我们可以为不同的接口配置不同的令牌桶,实现对不同接口的差异化限流。
  • 无需额外的存储设施:令牌桶算法的实现不需要额外的存储设施,只需要在内存中维护一个令牌桶即可,降低了系统的复杂度和成本。

缺点

  • 只适用于单机部署,分布式环境需要额外改造:该方案的令牌桶是在内存中维护的,因此只适用于单机部署的环境。在分布式环境中,需要将令牌桶的状态存储到共享的存储设施中,如 Redis,才能实现分布式限流。
  • 重启应用后状态丢失:由于令牌桶的状态是在内存中维护的,因此当应用重启后,令牌桶的状态会丢失,需要重新初始化。
  • 无法精确控制时间窗口内的请求总量:令牌桶算法只能控制请求的平均速率,无法精确控制在某个时间窗口内的请求总量。在某些对请求总量有严格限制的场景下,可能无法满足需求。

3. 分布式限流(Redis + Lua 脚本)

在分布式系统中,单机限流方案往往难以满足需求,因为不同的实例之间无法共享限流状态。利用 Redis 和 Lua 脚本可以实现高效的分布式限流,确保系统在分布式环境下的安全性和稳定性。

实现步骤

3.1 定义 Lua 脚本

首先,我们需要定义一个 Redis 限流的 Lua 脚本,用于实现限流逻辑。以下是具体的脚本内容:

在这个脚本中,我们首先获取限流键、限流窗口、限流阈值和当前时间戳。然后,我们移除过期的请求记录,统计当前窗口内的请求数。如果请求数超过了阈值,我们返回 0 表示拒绝请求。否则,我们添加当前请求记录,并设置过期时间,最后返回当前窗口剩余可用请求数。

3.2 创建 Redis 限流服务

接下来,我们需要创建一个 Redis 限流服务,用于执行 Lua 脚本并处理限流逻辑。以下是具体的代码实现:

@Service
@Slf4j
publicclass 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);
}

/**
* 尝试获取访问权限
* @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;
}
}
}

在这个服务中,我们使用 StringRedisTemplate 来执行 Lua 脚本。在 init 方法中,我们加载 Lua 脚本并设置返回值类型。在 isAllowed 方法中,我们执行 Lua 脚本并根据返回值判断是否允许访问。如果返回值为 -1 表示被限流,否则返回剩余可用请求数。

3.3 创建分布式限流注解

为了方便使用,我们可以创建一个分布式限流注解,用于标记需要进行分布式限流的接口方法。以下是具体的代码实现:

在这个注解中,我们可以通过 prefix 属性设置限流键的前缀,通过 window 属性设置时间窗口,通过 threshold 属性设置时间窗口内允许的最大请求数,通过 mode 属性设置限流模式。

3.4 实现分布式限流切面

接下来,我们需要实现一个 AOP 切面,用于处理分布式限流逻辑。以下是具体的代码实现:

@Aspect
@Component
@Slf4j
publicclass 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) {
thrownew 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()
returnnull;
}
}

在这个切面中,我们使用 @Around 注解拦截所有标记了 @DistributedRateLimit 注解的方法。在方法执行前,我们会根据注解中的参数生成限流键,并调用 RedisRateLimiterService 的 isAllowed 方法判断是否允许访问。如果不允许访问,我们会抛出一个异常,提示用户接口访问过于频繁。

3.5 使用示例

以下是一个使用 @DistributedRateLimit 注解的示例:

@RestController
@RequestMapping("/api")
publicclass 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();
}
}

在这个示例中,我们在 createPayment 方法上使用了 @DistributedRateLimit 注解,设置了 3600 秒内每个用户最多允许 5 次请求。在 getProducts 方法上,我们设置了 60 秒内每个 IP 最多允许 30 次请求。在 getHotResource 方法上,我们设置了 1 秒内整个接口最多允许 100 次请求。

优缺点分析

优点

  • 适用于分布式系统,多实例间共享限流状态:通过使用 Redis 作为共享存储,不同的实例可以共享限流状态,确保系统在分布式环境下的一致性和稳定性。
  • 支持多种限流模式:可以根据 IP、用户、接口总量等不同的维度进行限流,满足不同场景的需求。
  • 基于滑动窗口,计数更精确:Lua 脚本使用滑动窗口算法来统计请求数,相比固定窗口算法,计数更加精确,可以有效避免误判。
  • 使用 Lua 脚本保证原子性,避免竞态条件:Lua 脚本在 Redis 中是原子执行的,确保了限流逻辑的原子性,避免了多个实例同时修改限流状态时可能出现的竞态条件。

缺点

  • 强依赖 Redis:该方案的实现依赖于 Redis,如果 Redis 出现故障,可能会影响系统的正常运行。
  • 实现复杂度较高:需要编写 Lua 脚本并进行 Redis 操作,实现复杂度相对较高,对开发人员的技术要求也较高。

4. 集成 Sentinel 实现接口防刷

阿里巴巴开源的 Sentinel 是一个强大的流量控制组件,它就像一个智能的“交通警察”,可以对系统的流量进行实时监控和控制,提供了丰富的限流、熔断、系统保护等功能。

实现步骤

4.1 添加依赖

首先,我们需要在项目中添加 Sentinel 的依赖:

<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 的配置:

# Sentinel 控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8080
# 取消 Sentinel 控制台懒加载
spring.cloud.sentinel.eager=true
# 应用名称
spring.application.name=my-application

在这个配置中,我们设置了 Sentinel 控制台的地址,取消了控制台的懒加载,并指定了应用的名称。

4.3 创建 Sentinel 配置

接下来,我们需要创建一个 Sentinel 配置类,用于定义流控规则。以下是具体的代码实现:

在这个配置类中,我们创建了一个 SentinelResourceAspect 实例,并在 init 方法中定义了流控规则。对于 /api/user 接口,我们设置了每秒允许 10 个请求的限流规则。对于 /api/order 接口,我们设置了每秒允许 5 个请求的限流规则,并开启了预热模式,预热期为 10 秒。

4.4 创建 URL 资源解析器

为了让 Sentinel 能够正确识别请求的资源,我们需要创建一个 URL 资源解析器。以下是具体的代码实现:

@Component
publicclass UrlCleaner implements RequestOriginParser {

@Override
public String parseOrigin(HttpServletRequest request) {
// 获取请求的 URL 路径
String path = request.getRequestURI();

// 可以添加更复杂的解析逻辑,例如:
// 1. 去除路径变量:/api/user/123 -> /api/user/{id}
// 2. 添加请求方法前缀:GET:/api/user

return path;
}
}

在这个解析器中,我们简单地返回请求的 URL 路径,你可以根据实际需求添加更复杂的解析逻辑。

4.5 创建全局异常处理器

当请求被 Sentinel 限流时,会抛出 BlockException 异常,我们需要创建一个全局异常处理器来处理这个异常。以下是具体的代码实现:

@RestControllerAdvice
publicclass SentinelExceptionHandler {

@ExceptionHandler(BlockException.class)
public Result handleBlockException(BlockException e) {
String message = "请求过于频繁,请稍后再试";
if (e instanceof FlowException) {
message = "接口限流:" + message;
} elseif (e instanceof DegradeException) {
message = "服务降级:系统繁忙,请稍后再试";
} elseif (e instanceof ParamFlowException) {
message = "热点参数限流:请求过于频繁";
} elseif (e instanceof SystemBlockException) {
message = "系统保护:系统资源不足";
} elseif (e instanceof AuthorityException) {
message = "授权控制:没有访问权限";
}

return Result.error(429, message);
}
}

在这个异常处理器中,我们根据不同的 BlockException 类型返回不同的错误信息,提示用户请求被限流的原因。

4.6 使用 @SentinelResource 注解

为了让 Sentinel 能够对接口进行限流,我们需要在接口方法上使用 @SentinelResource 注解。以下是具体的代码实现:

@RestController
@RequestMapping("/api")
publicclass 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);
thrownew 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;
}
}

在这个示例中,我们在 getUser 方法上使用了 @SentinelResource 注解,指定了资源名、限流处理方法和异常回退方法。当请求被限流时,会调用 getUserBlockHandler 方法进行处理。当方法执行过程中出现异常时,会调用 getUserFallback 方法进行回退。

4.7 更复杂的限流规则配置

除了基本的流控规则,Sentinel 还支持更复杂的限流规则配置,如基于 QPS + 调用关系的限流规则、基于并发线程数的限流规则、热点参数限流规则等。以下是具体的代码实现:

@Service
@Slf4j
publicclass 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); // 可以有更高的 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);
}
}

在这个服务中,我们定义了基于 QPS + 调用关系的限流规则、基于并发线程数的限流规则和热点参数限流规则,并使用 FlowRuleManager 和 ParamFlowRuleManager 加载这些规则。

优缺点分析

优点

  • 功能全面:Sentinel 支持 QPS 限流、并发线程数限流、热点参数限流等多种限流方式,还提供了熔断、系统保护等功能,可以满足不同场景的需求。
  • 支持多种控制策略:可以选择直接拒绝、预热、排队等多种控制策略,根据实际情况灵活调整限流行为。
  • 提供控制台可视化管理:Sentinel 提供了可视化的控制台,可以实时监控系统的流量情况,方便进行规则的配置和管理。
  • 支持动态规则调整:可以通过控制台或 API 动态调整限流规则,无需重启应用,提高了系统的灵活性和可维护性。
  • 可与 Spring Cloud 体系无缝集成:Sentinel 可以与 Spring Cloud 体系无缝集成,方便在微服务架构中使用。

缺点

  • 学习曲线较陡峭:Sentinel 的功能丰富,配置复杂,对于初学者来说,学习成本较高。
  • 分布式场景下需要额外配置规则持久化:在分布式场景下,需要额外配置规则持久化,确保不同实例之间的规则一致性。
  • 引入了额外的依赖:集成 Sentinel 需要引入额外的依赖,增加了项目的复杂度和维护成本。

5. 验证码与行为分析防刷

对于某些敏感操作,如登录、注册、支付等,单纯的限流可能无法有效防止恶意请求。此时,可以结合验证码和行为分析来进一步增强系统的安全性,有效区分人类用户和自动化脚本。

实现步骤

5.1 图形验证码实现

首先,我们需要添加图形验证码的依赖:

<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>

5.2 创建验证码服务

接下来,我们需要创建一个验证码服务,用于生成和验证验证码。以下是具体的代码实现:

@Service
publicclass CaptchaService {

@Autowired
private StringRedisTemplate redisTemplate;

privatestaticfinallong 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);

// 生成验证码 ID
String captchaId = UUID.randomUUID().toString();

// 将验证码存入 Redis
redisTemplate.opsForValue().set(
"captcha:" + captchaId,
captcha.text().toLowerCase(),
CAPTCHA_EXPIRE_TIME,
TimeUnit.SECONDS
);

// 设置 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) {
returnfalse;
}

// 从 Redis 获取正确的验证码
String key = "captcha:" + captchaId;
String correctCode = redisTemplate.opsForValue().get(key);

// 验证成功后删除验证码
if (correctCode != null && correctCode.equals(captchaCode.toLowerCase())) {
redisTemplate.delete(key);
returntrue;
}

returnfalse;
}
}

在这个服务中,我们使用 easy-captcha 库生成验证码,并将验证码存入 Redis 中。在验证验证码时,我们从 Cookie 中获取验证码 ID,然后从 Redis 中获取正确的验证码进行比对。

5.3 创建验证码控制器

为了方便前端获取验证码,我们需要创建一个验证码控制器。以下是具体的代码实现:

@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);
}
}

在这个控制器中,我们提供了一个 GET 请求接口,用于生成验证码并返回 Base64 编码的验证码图片。

5.4 创建验证码注解

为了标记需要进行验证码验证的接口方法,我们可以创建一个验证码注解。以下是具体的代码实现:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CaptchaRequired {
String captchaParam() default "captchaCode";
}

在这个注解中,我们可以通过 captchaParam 属性指定验证码参数的名称。

5.5 实现验证码拦截器

接下来,我们需要实现一个验证码拦截器,在请求进入接口之前进行验证码验证。以下是具体的代码实现:

@Component
publicclass CaptchaInterceptor implements HandlerInterceptor {

@Autowired
private CaptchaService captchaService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
returntrue;
}

HandlerMethod handlerMethod = (HandlerMethod) handler;
CaptchaRequired captchaRequired = handlerMethod.getMethodAnnotation(CaptchaRequired.class);

if (captchaRequired == null) {
returntrue;
}

// 获取验证码参数
String captchaParam = captchaRequired.captchaParam();
String captchaCode = request.getParameter(captchaParam);

if (StringUtils.hasText(captchaCode)) {
// 验证验证码
boolean valid = captchaService.validateCaptcha(request, captchaCode);
if (valid) {
returntrue;
}
}

// 验证失败
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.getWriter().write("{\"code\":400,\"message\":\"验证码错误或已过期\"}");
returnfalse;
}
}

在这个拦截器中,我们首先判断请求的处理方法是否标记了 @CaptchaRequired 注解。如果标记了,则获取验证码参数并调用 CaptchaService 的 validateCaptcha 方法进行验证。如果验证失败,则返回一个错误响应,提示用户验证码错误或已过期。

5.6 创建行为分析服务

除了验证码,我们还可以通过行为分析来检测可疑的机器行为。以下是具体的代码实现:

@Service
@Slf4j
publicclass 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);
returntrue;
}

// 3. 检查 User-Agent
if (userAgent == null || isBotuserAgent(userAgent)) {
log.warn("可疑的 User-Agent: {}", userAgent);
returntrue;
}

// 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);
returntrue;
}
}

redisTemplate.opsForValue().set(timeKey, String.valueOf(now), 10, TimeUnit.MINUTES);

// 更多高级检测逻辑...

returnfalse;
}

/**
* 检查是否是机器人 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) {
returnfalse;
}

// 计算间隔的方差,方差小说明请求间隔很均匀
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 方法同上
}

在这个服务中,我们从多个维度对请求进行分析,包括访问频率、User-Agent 和请求时间模式。如果发现可疑的行为,我们会记录日志并返回 true。

5.7 创建行为分析拦截器

最后,我们需要创建一个行为分析拦截器,在请求进入接口之前进行行为分析。以下是具体的代码实现:

@Component
publicclass 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}");
returnfalse;
}
}

returntrue;
}

/**
* 判断是否是高风险端点
*/
private boolean isPotentialRiskEndpoint(String path) {
return path.contains("/login") ||
path.contains("/register") ||
path.contains("/payment") ||
path.contains("/order") ||
path.contains("/password");
}
}

在这个拦截器中,我们首先判断请求的 URI 是否以 /api/ 开头,并且是否是高风险端点。如果是,则调用 BehaviorAnalysisService 的 isSuspicious 方法进行行为分析。如果发现可疑行为,则返回一个错误响应,提示用户进行验证。

5.8 使用示例

以下是一个使用验证码和行为分析的示例:

@RestController
@RequestMapping("/api")
publicclass 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);
}
}

在这个示例中,我们在 login 和 register 方法上使用了 @CaptchaRequired 注解,要求用户输入验证码进行验证。

优缺点分析

优点

  • 能有效区分人类用户和自动化脚本:通过验证码和行为分析,可以准确地识别出自动化脚本的恶意请求,有效防止刷接口行为。
  • 对恶意用户有较强的阻止作用:验证码和行为分析的双重防护,大大增加了恶意用户的攻击成本,使其难以得逞。
  • 针对敏感操作提供额外安全层:对于登录、注册、支付等敏感操作,验证码和行为分析可以提供额外的安全保障,保护用户的隐私和财产安全。
  • 可以实现自适应安全策略:根据用户的行为特征和系统的安全状况,可以动态调整验证码和行为分析的策略,实现自适应的安全防护。

缺点

  • 增加了用户操作成本,可能影响用户体验:要求用户输入验证码会增加用户的操作步骤,降低用户体验。特别是对于一些频繁操作的用户,可能会感到厌烦。
  • 实现复杂,需要前后端配合:验证码和行为分析的实现涉及到前后端的多个环节,需要前后端密切配合,增加了开发和维护的难度。
  • 某些验证码可能被 OCR 技术破解:虽然现在的验证码技术不断发展,但仍然存在被 OCR 技术破解的风险,需要不断更新和改进验证码的生成和验证方式。
  • 行为分析可能产生误判:行为分析是基于一定的规则和算法进行的,可能会出现误判的情况,将正常用户的行为误判为可疑行为,影响用户的正常使用。

方案对比与选择

方案 实现难度 防刷效果 分布式支持 用户体验 适用场景
基于注解的访问频率限制 低 中 需配合 Redis 一般 一般接口,简单场景
令牌桶算法 中 中高 单机 好 允许突发流量的场景
分布式限流(Redis+Lua) 高 高 支持 一般 分布式系统,精确限流
Sentinel 中高 高 需额外配置 可配置 复杂系统,多维度防护
验证码与行为分析 高 高 支持 较差 敏感操作,关键业务

总结

接口防刷是一个系统性工程,需要综合考虑安全性、用户体验、性能开销和运维复杂度等多方面因素。本文介绍的 5 种方案各有优缺点,你可以根据实际需求灵活选择和组合。

无论采用哪种方案,接口防刷都应该遵循以下原则:

  • 最小影响原则:尽量不影响正常用户的体验,确保系统在安全的前提下能够高效运行。
  • 梯度防护原则:根据接口的重要程度和风险等级,采用不同强度的防护措施,实现精准防护。
  • 可监控原则:提供充分的监控和告警机制,及时发现和处理异常情况,确保系统的安全性和稳定性。
  • 灵活调整原则:支持动态调整防护参数和策略,根据系统的运行情况和安全需求,及时做出调整。

通过合理实施接口防刷策略,可以有效提高系统的安全性和稳定性,为用户提供更好的服务体验。希望本文能对你有所帮助,让你的系统在复杂的网络环境中更加安全可靠!

文章目录
  1. 1. 1. 基于注解的访问频率限制
    1. 1.1. 实现步骤
      1. 1.1.1. 1.1 创建限流注解
      2. 1.1.2. 1.2 实现限流切面
      3. 1.1.3. 1.3 使用示例
    2. 1.2. 优缺点分析
      1. 1.2.1. 优点
      2. 1.2.2. 缺点
  2. 2. 2. 令牌桶算法实现限流
    1. 2.1. 实现步骤
      1. 2.1.1. 2.1 引入依赖
      2. 2.1.2. 2.2 创建令牌桶限流器
      3. 2.1.3. 2.3 创建拦截器
      4. 2.1.4. 2.4 配置拦截器
    2. 2.2. 优缺点分析
      1. 2.2.1. 优点
      2. 2.2.2. 缺点
  3. 3. 3. 分布式限流(Redis + Lua 脚本)
    1. 3.1. 实现步骤
      1. 3.1.1. 3.1 定义 Lua 脚本
      2. 3.1.2. 3.2 创建 Redis 限流服务
      3. 3.1.3. 3.3 创建分布式限流注解
      4. 3.1.4. 3.4 实现分布式限流切面
      5. 3.1.5. 3.5 使用示例
    2. 3.2. 优缺点分析
      1. 3.2.1. 优点
      2. 3.2.2. 缺点
  4. 4. 4. 集成 Sentinel 实现接口防刷
    1. 4.1. 实现步骤
      1. 4.1.1. 4.1 添加依赖
      2. 4.1.2. 4.2 配置 Sentinel
      3. 4.1.3. 4.3 创建 Sentinel 配置
      4. 4.1.4. 4.4 创建 URL 资源解析器
      5. 4.1.5. 4.5 创建全局异常处理器
      6. 4.1.6. 4.6 使用 @SentinelResource 注解
      7. 4.1.7. 4.7 更复杂的限流规则配置
    2. 4.2. 优缺点分析
      1. 4.2.1. 优点
      2. 4.2.2. 缺点
  5. 5. 5. 验证码与行为分析防刷
    1. 5.1. 实现步骤
      1. 5.1.1. 5.1 图形验证码实现
      2. 5.1.2. 5.2 创建验证码服务
      3. 5.1.3. 5.3 创建验证码控制器
      4. 5.1.4. 5.4 创建验证码注解
      5. 5.1.5. 5.5 实现验证码拦截器
      6. 5.1.6. 5.6 创建行为分析服务
      7. 5.1.7. 5.7 创建行为分析拦截器
      8. 5.1.8. 5.8 使用示例
    2. 5.2. 优缺点分析
      1. 5.2.1. 优点
      2. 5.2.2. 缺点
  6. 6. 方案对比与选择
  7. 7. 总结