本文目录导读:

我来提供一个完整的Java防刷案例,包含多种防刷策略:
基础防刷框架
1 限流注解定义
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
// 限流时间窗口(秒)
int time() default 60;
// 最大访问次数
int count() default 10;
// 限流类型
LimitType limitType() default LimitType.IP;
enum LimitType {
IP, // 按IP限流
USER, // 按用户限流
IP_USER, // IP+用户组合限流
CUSTOM // 自定义
}
// 限流提示信息
String message() default "请求过于频繁,请稍后再试";
}
2 滑动窗口限流器
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
public class SlidingWindowRateLimiter {
// 窗口划分的粒度(毫秒)
private static final int WINDOW_BUCKETS = 10;
// 存储每个key的滑动窗口数据
private final Map<String, SlidingWindow> windows = new ConcurrentHashMap<>();
private ReentrantLock lock = new ReentrantLock();
/**
* 尝试获取许可
* @param key 限流key(如IP、用户ID)
* @param windowSize 窗口大小(毫秒)
* @param maxCount 最大请求数
* @return 是否允许通过
*/
public boolean tryAcquire(String key, long windowSize, int maxCount) {
long currentTime = System.currentTimeMillis();
// 计算当前时间所在的桶
long bucketSize = windowSize / WINDOW_BUCKETS;
int currentBucket = (int) ((currentTime % windowSize) / bucketSize);
SlidingWindow window = windows.computeIfAbsent(key,
k -> new SlidingWindow(windowSize, bucketSize, maxCount));
return window.tryAcquire(currentBucket);
}
class SlidingWindow {
// 每个桶的请求计数
private final AtomicInteger[] buckets;
// 每个桶的起始时间
private final long[] bucketTimes;
// 窗口大小
private final long windowSize;
// 桶大小
private final long bucketSize;
// 最大请求数
private final int maxCount;
public SlidingWindow(long windowSize, long bucketSize, int maxCount) {
this.windowSize = windowSize;
this.bucketSize = bucketSize;
this.maxCount = maxCount;
this.buckets = new AtomicInteger[WINDOW_BUCKETS];
this.bucketTimes = new long[WINDOW_BUCKETS];
long currentTime = System.currentTimeMillis();
for (int i = 0; i < WINDOW_BUCKETS; i++) {
buckets[i] = new AtomicInteger(0);
bucketTimes[i] = currentTime - (WINDOW_BUCKETS - 1 - i) * bucketSize;
}
}
public synchronized boolean tryAcquire(int currentBucket) {
long currentTime = System.currentTimeMillis();
// 清理过期的桶
for (int i = 0; i < WINDOW_BUCKETS; i++) {
if (currentTime - bucketTimes[i] >= windowSize) {
buckets[i].set(0);
bucketTimes[i] = currentTime - (currentTime % bucketSize);
}
}
// 计算当前窗口内的总请求数
int totalCount = 0;
for (int i = 0; i < WINDOW_BUCKETS; i++) {
totalCount += buckets[i].get();
}
// 如果达到上限,拒绝请求
if (totalCount >= maxCount) {
return false;
}
// 增加当前桶的计数
buckets[currentBucket].incrementAndGet();
return true;
}
}
}
拦截器实现
1 限流拦截器
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final SlidingWindowRateLimiter rateLimiter;
private final RedisRateLimiter redisRateLimiter;
public RateLimitInterceptor() {
this.rateLimiter = new SlidingWindowRateLimiter();
this.redisRateLimiter = new RedisRateLimiter();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 如果不是HandlerMethod,直接放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 获取方法上的限流注解
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
if (rateLimit == null) {
// 检查类级别的注解
rateLimit = method.getDeclaringClass().getAnnotation(RateLimit.class);
}
// 没有限流注解,直接放行
if (rateLimit == null) {
return true;
}
// 生成限流key
String key = generateKey(rateLimit, request);
// 检查是否被限流
boolean allowed = checkRateLimit(key, rateLimit);
if (!allowed) {
// 返回限流提示
handleRateLimitExceeded(response, rateLimit);
return false;
}
return true;
}
private String generateKey(RateLimit rateLimit, HttpServletRequest request) {
StringBuilder keyBuilder = new StringBuilder();
// 根据限流类型生成key
switch (rateLimit.limitType()) {
case IP:
keyBuilder.append(getClientIp(request));
break;
case USER:
// 获取当前登录用户ID
String userId = getCurrentUserId();
keyBuilder.append(userId != null ? userId : "anonymous");
break;
case IP_USER:
keyBuilder.append(getClientIp(request)).append(":")
.append(getCurrentUserId() != null ? getCurrentUserId() : "anonymous");
break;
default:
keyBuilder.append(getClientIp(request));
}
return keyBuilder.toString();
}
private boolean checkRateLimit(String key, RateLimit rateLimit) {
// 使用滑动窗口限流
boolean slidingWindowResult = rateLimiter.tryAcquire(
key,
rateLimit.time() * 1000L,
rateLimit.count()
);
// 使用Redis分布式限流(可选)
boolean redisResult = redisRateLimiter.tryAcquire(
key,
rateLimit.time(),
rateLimit.count()
);
// 两个限流器都通过才放行
return slidingWindowResult && redisResult;
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 处理多级代理的情况
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
private String getCurrentUserId() {
// 从SecurityContext中获取当前用户
// 这里需要根据实际情况实现
return null;
}
private void handleRateLimitExceeded(HttpServletResponse response, RateLimit rateLimit)
throws Exception {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(String.format(
"{\"code\":429,\"message\":\"%s\"}",
rateLimit.message()
));
}
}
Redis分布式限流器
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
@Component
public class RedisRateLimiter {
private final RedisTemplate<String, Object> redisTemplate;
// Lua脚本实现原子限流
private final String LIMIT_SCRIPT =
"local key = KEYS[1]\n" +
"local count = tonumber(ARGV[1])\n" +
"local timeWindow = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"local keyIncr = key .. ':incr'\n" +
"local keyLastTime = key .. ':lastTime'\n" +
"local lastTime = tonumber(redis.call('get', keyLastTime) or '0')\n" +
"\n" +
"if lastTime == 0 or (now - lastTime) >= timeWindow then\n" +
" redis.call('set', keyIncr, '1')\n" +
" redis.call('set', keyLastTime, now)\n" +
" return 1\n" +
"end\n" +
"\n" +
"local currentCount = tonumber(redis.call('incr', keyIncr))\n" +
"if currentCount <= count then\n" +
" return 1\n" +
"end\n" +
"\n" +
"return 0";
public RedisRateLimiter() {
this.redisTemplate = new RedisTemplate<>();
}
/**
* 尝试获取许可
* @param key 限流key
* @param timeWindow 时间窗口(秒)
* @param maxCount 最大请求数
* @return 是否允许通过
*/
public boolean tryAcquire(String key, int timeWindow, int maxCount) {
try {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LIMIT_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList("rate:limit:" + key),
String.valueOf(maxCount),
String.valueOf(timeWindow),
String.valueOf(System.currentTimeMillis() / 1000)
);
return result != null && result > 0;
} catch (Exception e) {
// Redis异常时,默认放行(降级策略)
return true;
}
}
/**
* 使用固定窗口计数
* @param key 限流key
* @param timeWindow 时间窗口(秒)
* @param maxCount 最大请求数
*/
public boolean tryAcquireFixedWindow(String key, int timeWindow, int maxCount) {
String redisKey = "rate:fixed:" + key + ":" +
(System.currentTimeMillis() / (timeWindow * 1000));
Long count = redisTemplate.opsForValue().increment(redisKey);
if (count != null && count == 1) {
// 设置过期时间
redisTemplate.expire(redisKey, timeWindow, java.util.concurrent.TimeUnit.SECONDS);
}
return count != null && count <= maxCount;
}
}
使用示例
1 控制器中使用
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class DemoController {
// 基础限流:60秒内最多10次
@GetMapping("/search")
@RateLimit(time = 60, count = 10,
limitType = RateLimit.LimitType.IP,
message = "搜索过于频繁,请稍后再试")
public Result search(@RequestParam String keyword) {
return Result.success("搜索结果");
}
// 用户限流:30秒内最多5次
@PostMapping("/login")
@RateLimit(time = 30, count = 5,
limitType = RateLimit.LimitType.IP_USER,
message = "登录尝试过多,账号暂时锁定")
public Result login(@RequestBody LoginRequest request) {
return Result.success("登录成功");
}
// 注解配置限流
@GetMapping("/hot-data")
@RateLimit(time = 10, count = 5, message = "访问过于频繁")
public Result getHotData() {
return Result.success("热点数据");
}
// 自定义限流参数
@GetMapping("/custom")
@RateLimit(time = 60, count = 20,
limitType = RateLimit.LimitType.CUSTOM,
message = "请求过多")
public Result customEndpoint(HttpServletRequest request) {
// 自定义key的逻辑在拦截器中实现
return Result.success("自定义限流");
}
}
2 配置文件
# application.yml
spring:
redis:
host: localhost
port: 6379
timeout: 5000
lettuce:
pool:
max-active: 100
max-idle: 20
min-idle: 5
# 自定义限流配置
rate-limit:
enabled: true
default-time-window: 60
default-max-count: 10
redis-enabled: true
sliding-window-enabled: true
3 配置拦截器注册
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final RateLimitInterceptor rateLimitInterceptor;
public WebConfig(RateLimitInterceptor rateLimitInterceptor) {
this.rateLimitInterceptor = rateLimitInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**", "/api/login", "/static/**");
}
}
其他防刷策略
1 验证码防刷
@Service
public class CaptchaService {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 生成并发送验证码
*/
public String generateCaptcha(String phone) {
// 限制发送频率
String frequencyKey = "captcha:frequency:" + phone;
String freqCount = (String) redisTemplate.opsForValue().get(frequencyKey);
if (freqCount != null && Integer.parseInt(freqCount) >= 3) {
throw new BusinessException("验证码发送过于频繁");
}
// 生成验证码
String code = generateCode();
// 存储验证码(5分钟有效期)
redisTemplate.opsForValue().set("captcha:code:" + phone, code, 5, TimeUnit.MINUTES);
// 记录发送次数(1小时内最多3次)
if (freqCount == null) {
redisTemplate.opsForValue().set(frequencyKey, "1", 1, TimeUnit.HOURS);
} else {
redisTemplate.opsForValue().increment(frequencyKey);
}
// 发送验证码短信
sendSms(phone, code);
return code;
}
private String generateCode() {
// 生成6位随机数字
return String.valueOf((int) ((Math.random() * 9 + 1) * 100000));
}
private void sendSms(String phone, String code) {
// 调用短信服务
}
}
2 IP黑名单与白名单
@Component
public class IpFilterService {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 检查IP是否被禁止
*/
public boolean isBlockedIp(String ip) {
String blockedKey = "ip:blocked:" + ip;
return Boolean.TRUE.equals(redisTemplate.hasKey(blockedKey));
}
/**
* 记录异常请求
*/
public void recordAbnormalRequest(String ip) {
String key = "ip:abnormal:" + ip;
Long count = redisTemplate.opsForValue().increment(key);
if (count != null && count > 10) {
// 超过10次异常,封禁1小时
redisTemplate.opsForValue().set("ip:blocked:" + ip, true, 1, TimeUnit.HOURS);
redisTemplate.delete(key);
} else {
// 设置过期时间为1小时
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
}
/**
* 添加白名单
*/
public void addToWhitelist(String ip) {
redisTemplate.opsForSet().add("ip:whitelist", ip);
}
/**
* 是否在白名单
*/
public boolean isWhitelisted(String ip) {
return Boolean.TRUE.equals(
redisTemplate.opsForSet().isMember("ip:whitelist", ip)
);
}
}
3 签名验证防刷
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
@Service
public class SignatureService {
private static final String SECRET_KEY = "your-secret-key";
/**
* 验证请求签名
*/
public boolean verifySignature(Map<String, String> params, String signature,
String timestamp) {
// 检查时间戳是否过期(5分钟)
long ts = Long.parseLong(timestamp);
if (System.currentTimeMillis() - ts > 300000) {
return false;
}
// 防止重复请求(使用Redis)
String nonce = params.get("nonce");
String nonceKey = "nonce:" + nonce;
if (Boolean.TRUE.equals(redisTemplate.hasKey(nonceKey))) {
return false;
}
redisTemplate.opsForValue().set(nonceKey, true, 300, TimeUnit.SECONDS);
// 生成签名
String generateSignature = generateSignature(params, timestamp);
return generateSignature.equals(signature);
}
private String generateSignature(Map<String, String> params, String timestamp) {
// 参数排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
sortedParams.put("timestamp", timestamp);
// 拼接字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
// 去除最后的&
String stringToSign = sb.substring(0, sb.length() - 1);
// 使用HMAC-SHA256生成签名
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "HmacSHA256");
mac.init(keySpec);
byte[] hash = mac.doFinal(stringToSign.getBytes());
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException("签名生成失败", e);
}
}
}
统一异常处理
@ControllerAdvice
public class RateLimitExceptionHandler {
@ExceptionHandler(RateLimitException.class)
@ResponseBody
public Result handleRateLimit(RateLimitException e) {
return Result.error(429, e.getMessage());
}
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result handleBusiness(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
}
最佳实践建议
- 多级限流:IP限流 + 用户限流 + 接口限流相结合
- 降级策略:Redis不可用时自动降级为本地限流
- 动态配置:支持通过配置中心动态调整限流参数
- 监控告警:实时监控限流触发情况并告警
- 平滑限流:使用令牌桶算法实现平滑限流
- 数据清理:定期清理过期的限流数据
这个案例提供了完整的防刷方案,可以根据实际业务需求进行扩展和调整。