SpringBoot实现接口防刷

在SpringBoot中实现接口防刷(限流)可以有效保护系统免受恶意攻击或过度请求,以下是几种常见的实现方案:

一、基于Redis的令牌桶算法(推荐)

令牌桶算法允许一定程度的突发请求,适合大多数业务场景。通过Redis实现分布式限流:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class RateLimiter {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

// 令牌桶算法Lua脚本
private static final String TOKEN_BUCKET_SCRIPT =
"local tokens_key = KEYS[1]\n" +
"local timestamp_key = KEYS[2]\n" +
"local rate = tonumber(ARGV[1])\n" +
"local capacity = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"local requested = tonumber(ARGV[4])\n" +
"local last_tokens = tonumber(redis.call('get', tokens_key) or capacity)\n" +
"local last_refreshed = tonumber(redis.call('get', timestamp_key) or 0)\n" +
"local delta = math.max(0, now - last_refreshed)\n" +
"local filled_tokens = math.min(capacity, last_tokens + (delta * rate / 1000))\n" +
"local allowed = filled_tokens >= requested\n" +
"local new_tokens = filled_tokens\n" +
"if allowed then\n" +
" new_tokens = filled_tokens - requested\n" +
"end\n" +
"redis.call('set', tokens_key, new_tokens)\n" +
"redis.call('set', timestamp_key, now)\n" +
"return allowed";

private final RedisScript<Boolean> script = new DefaultRedisScript<>(TOKEN_BUCKET_SCRIPT, Boolean.class);

/**
* 限流检查
* @param key 限流标识(如用户ID、IP)
* @param rate 每秒生成的令牌数
* @param capacity 令牌桶容量
* @return 是否允许访问
*/
public boolean isAllowed(String key, int rate, int capacity) {
List<String> keys = Arrays.asList(key + ".tokens", key + ".timestamp");
long now = System.currentTimeMillis();
return redisTemplate.execute(script, keys, rate, capacity, now, 1);
}
}

使用示例

@RestController
@RequestMapping("/api")
public class UserController {

@Autowired
private RateLimiter rateLimiter;

@GetMapping("/user/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// 对每个用户ID进行限流,每秒2次请求,令牌桶容量10
String limitKey = "user:" + id;
if (!rateLimiter.isAllowed(limitKey, 2, 10)) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
// 处理正常业务逻辑
return ResponseEntity.ok(userService.getUserById(id));
}
}

二、基于Spring AOP的注解式限流

通过自定义注解和AOP实现更优雅的限流控制:

1. 定义限流注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int rate() default 10; // 每秒生成的令牌数
int capacity() default 100; // 令牌桶容量
String key() default ""; // 限流标识,默认为方法名
}

2. AOP切面实现

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
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.lang.reflect.Method;

@Aspect
@Component
public class RateLimitAspect {

@Autowired
private RateLimiter rateLimiter;

@Around("@annotation(com.example.demo.RateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);

// 生成限流Key(可根据IP、用户ID等自定义)
String key = generateKey(rateLimit, joinPoint);

if (!rateLimiter.isAllowed(key, rateLimit.rate(), rateLimit.capacity())) {
throw new RuntimeException("请求过于频繁,请稍后再试");
}

return joinPoint.proceed();
}

private String generateKey(RateLimit rateLimit, ProceedingJoinPoint joinPoint) {
if (!"".equals(rateLimit.key())) {
return rateLimit.key();
}
// 默认使用IP+方法名作为Key
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String ip = request.getRemoteAddr();
String methodName = joinPoint.getSignature().getName();
return "rate_limit:" + ip + ":" + methodName;
}
}

3. 使用注解限流

@RestController
@RequestMapping("/api")
public class OrderController {

@RateLimit(rate = 5, capacity = 20) // 每秒5次请求,容量20
@PostMapping("/order")
public ResponseEntity<?> createOrder(@RequestBody OrderDTO order) {
// 业务逻辑
return ResponseEntity.ok().build();
}
}

三、基于Sentinel的流量控制

集成Alibaba Sentinel实现更强大的限流、熔断和降级:

1. 添加依赖

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-starter</artifactId>
<version>1.8.6</version>
</dependency>

2. 配置规则

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

@Configuration
public class SentinelConfig {

@PostConstruct
public void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();

// 定义接口限流规则
FlowRule rule = new FlowRule();
rule.setResource("/api/user/*"); // 资源名,可使用URL模式
rule.setCount(20); // 限流阈值(QPS)
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 限流类型:QPS
rule.setLimitApp("default"); // 针对所有调用方
rules.add(rule);

FlowRuleManager.loadRules(rules);
}
}

3. 处理限流异常

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class SentinelBlockHandler implements BlockExceptionHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws Exception {
response.setContentType("application/json;charset=UTF-8");

if (ex instanceof FlowException) {
response.setStatus(429);
response.getWriter().print("{\"code\":429,\"message\":\"请求过于频繁\"}");
} else {
response.setStatus(500);
response.getWriter().print("{\"code\":500,\"message\":\"系统繁忙,请稍后再试\"}");
}
}
}

四、基于Guava的本地限流(单机环境)

如果是单机应用,可使用Guava的RateLimiter:

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;

@Component
public class LocalRateLimiter {

// 创建每秒生成10个令牌的限流器
private final RateLimiter rateLimiter = RateLimiter.create(10.0);

/**
* 尝试获取令牌
* @return 是否获取成功
*/
public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
}

使用示例

@RestController
@RequestMapping("/api")
public class TestController {

@Autowired
private LocalRateLimiter localRateLimiter;

@GetMapping("/test")
public ResponseEntity<?> test() {
if (!localRateLimiter.tryAcquire()) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
return ResponseEntity.ok().build();
}
}

五、总结与最佳实践

  1. 分布式系统推荐方案:Redis + 令牌桶算法(方案一)或 Sentinel(方案三)
  2. 单机应用推荐方案:Guava RateLimiter(方案四)
  3. 注解式开发:结合AOP和自定义注解(方案二)提升代码可维护性
  4. 监控与告警:Sentinel提供可视化监控界面,方便查看限流情况和调整规则
  5. 熔断与降级:复杂场景下,考虑结合熔断机制(如Sentinel的熔断策略)防止级联故障

根据系统规模和需求选择合适的限流方案,通常分布式系统优先考虑Redis或Sentinel,而中小型应用可使用Guava实现简单限流。