Java防刷案例

wen java案例 2

本文目录导读:

Java防刷案例

  1. 基础防刷框架
  2. 拦截器实现
  3. Redis分布式限流器
  4. 使用示例
  5. 其他防刷策略
  6. 统一异常处理
  7. 最佳实践建议

我来提供一个完整的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());
    }
}

最佳实践建议

  1. 多级限流:IP限流 + 用户限流 + 接口限流相结合
  2. 降级策略:Redis不可用时自动降级为本地限流
  3. 动态配置:支持通过配置中心动态调整限流参数
  4. 监控告警:实时监控限流触发情况并告警
  5. 平滑限流:使用令牌桶算法实现平滑限流
  6. 数据清理:定期清理过期的限流数据

这个案例提供了完整的防刷方案,可以根据实际业务需求进行扩展和调整。

抱歉,评论功能暂时关闭!