本文目录导读:

我来提供一个完整的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不可用时自动降级为本地限流
- 动态配置:支持通过配置中心动态调整限流参数
- 监控告警:实时监控限流触发情况并告警
- 平滑限流:使用令牌桶算法实现平滑限流
- 数据清理:定期清理过期的限流数据
这个案例提供了完整的防刷方案,可以根据实际业务需求进行扩展和调整。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播