SpringBoot + 接口防刷 + 滑动窗口计数:登录、短信、支付接口防暴力攻击

引言

在互联网应用中,接口安全是一个永恒的话题。你有没有遇到过这种情况:用户疯狂点击登录按钮导致服务器压力过大,或者恶意刷短信验证码造成成本损失?这些问题的根源就是缺乏有效的接口防刷机制。

今天就来聊聊如何用SpringBoot结合Redis实现滑动窗口计数算法,为登录、短信、支付等关键接口建立坚固的防护墙,让你的系统在面对暴力攻击时依然稳如泰山。

为什么需要接口防刷?

接口暴力攻击的危害

让我们先看看没有防刷机制的系统面临什么风险:

服务器资源浪费

  • 恶意用户不断发起请求,消耗大量CPU和内存
  • 数据库连接池被占满,影响正常用户访问
  • 网络带宽被恶意请求占用

业务成本增加

  • 短信验证码被大量刷取,产生巨额费用
  • 第三方API调用次数超限,影响业务正常运行
  • 服务器扩容成本增加

用户体验下降

  • 正常用户的请求被恶意请求挤占
  • 系统响应变慢,甚至出现服务不可用
  • 影响业务正常运营

数据安全风险

  • 暴力破解密码尝试
  • 恶意刷取优惠券或积分
  • 爬虫批量抓取敏感数据

滑动窗口计数的优势

精准控制

  • 精确统计任意时间窗口内的请求次数
  • 避免固定窗口算法的边界问题
  • 实时响应请求频率变化

资源友好

  • 只保留时间窗口内的数据
  • 自动清理过期请求记录
  • 内存使用可控

灵活性强

  • 可根据不同接口设置不同限制
  • 支持动态调整限流参数
  • 适应各种业务场景需求

核心架构设计

我们的滑动窗口计数防刷架构:

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   用户请求      │───▶│   拦截器/切面    │───▶│   Redis存储    │
│  (Controller)   │    │ (RateLimiter)   │    │  (ZSET结构)     │
└─────────────────┘    └──────────────────┘    └─────────────────┘
        │                        │                       │
        │ 发起请求               │                       │
        │───────────────────────▶│                       │
        │                        │ 检查请求频率          │
        │                        │──────────────────────▶│
        │                        │                       │
        │                        │ 查询ZSET中时间段内    │
        │                        │──────────────────────▶│
        │                        │                       │
        │                        │ 统计请求数量          │
        │                        │──────────────────────▶│
        │                        │                       │
        │                        │ 判断是否超限          │
        │                        │──────────────────────▶│
        │                        │                       │
        │ 请求成功或拒绝         │                       │
        │◀───────────────────────│                       │
        │                        │                       │

核心设计要点

1. 滑动窗口计数算法

// 滑动窗口计数器服务
@Service
@Slf4j
public class SlidingWindowCounter {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 检查指定用户在时间窗口内的请求次数
     *
     * @param key       限流标识(如:用户ID、IP地址)
     * @param windowMs  时间窗口(毫秒)
     * @param maxCount  最大请求数
     * @return true表示允许请求,false表示超出限制
     */
    public boolean isAllowed(String key, long windowMs, int maxCount) {
        try {
            // 当前时间戳
            long currentTime = System.currentTimeMillis();
            // 窗口开始时间
            long windowStart = currentTime - windowMs;

            // 使用Redis ZSET存储请求记录,score为时间戳,value为唯一标识
            String redisKey = "rate_limit:" + key;

            // 删除窗口之前的旧记录
            redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);

            // 获取当前窗口内的请求数
            Long currentCount = redisTemplate.opsForZSet().count(redisKey, windowStart, currentTime);

            if (currentCount != null && currentCount >= maxCount) {
                log.warn("请求被限制: key={}, windowMs={}, maxCount={}, currentCount={}", 
                    key, windowMs, maxCount, currentCount);
                return false;
            }

            // 添加当前请求记录
            redisTemplate.opsForZSet().add(redisKey, generateUniqueValue(), currentTime);
            
            // 设置过期时间,避免key永久存在
            redisTemplate.expire(redisKey, Duration.ofMillis(windowMs + 60000)); // 多保留1分钟

            log.debug("请求通过: key={}, currentCount={}", key, (currentCount != null ? currentCount + 1 : 1));
            return true;

        } catch (Exception e) {
            log.error("滑动窗口计数检查异常", e);
            // 发生异常时,默认允许请求,避免影响正常业务
            return true;
        }
    }

    /**
     * 使用Lua脚本原子性执行滑动窗口检查
     * 这样可以避免多次Redis网络往返,提高性能
     */
    public boolean isAllowedAtomic(String key, long windowMs, int maxCount) {
        String script = 
            "local key = KEYS[1] " +
            "local window_start = ARGV[1] " +
            "local current_time = ARGV[2] " +
            "local max_count = ARGV[3] " +
            
            // 删除过期数据
            "redis.call('ZREMRANGEBYSCORE', key, 0, window_start) " +
            
            // 统计当前窗口内请求数
            "local current_count = redis.call('ZCOUNT', key, window_start, current_time) " +
            
            // 检查是否超过限制
            "if tonumber(current_count) >= tonumber(max_count) then " +
                "return 0 " +  // 超限
            "else " +
                "redis.call('ZADD', key, current_time, current_time .. ':' .. math.random(1000000)) " +
                "redis.call('EXPIRE', key, 60) " +  // 设置过期时间
                "return 1 " +  // 允许
            "end";

        try {
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            redisScript.setResultType(Long.class);

            Long result = redisTemplate.execute(redisScript, 
                Collections.singletonList("rate_limit:" + key),
                String.valueOf(System.currentTimeMillis() - windowMs),
                String.valueOf(System.currentTimeMillis()),
                String.valueOf(maxCount)
            );

            boolean allowed = result != null && result == 1L;
            if (!allowed) {
                log.warn("请求被限制: key={}, windowMs={}, maxCount={}", key, windowMs, maxCount);
            }
            return allowed;

        } catch (Exception e) {
            log.error("原子性滑动窗口计数检查异常", e);
            return true; // 异常时默认允许
        }
    }

    /**
     * 生成唯一值,用于区分同一时刻的不同请求
     */
    private String generateUniqueValue() {
        return System.currentTimeMillis() + ":" + ThreadLocalRandom.current().nextLong(1000000);
    }

    /**
     * 获取当前窗口内的请求数
     */
    public long getCurrentCount(String key, long windowMs) {
        try {
            long currentTime = System.currentTimeMillis();
            long windowStart = currentTime - windowMs;
            String redisKey = "rate_limit:" + key;

            Long count = redisTemplate.opsForZSet().count(redisKey, windowStart, currentTime);
            return count != null ? count : 0L;
        } catch (Exception e) {
            log.error("获取当前请求数异常", e);
            return 0L;
        }
    }
}

2. 限流注解定义

// 限流注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    
    /**
     * 限流的key,支持SpEL表达式
     */
    String key() default "";
    
    /**
     * 时间窗口大小(毫秒)
     */
    long window() default 60000; // 默认1分钟
    
    /**
     * 时间窗口内最大请求数
     */
    int count() default 10;
    
    /**
     * 超限时的提示信息
     */
    String message() default "请求过于频繁,请稍后再试";
    
    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
    
    enum LimitType {
        /**
         * 默认策略,使用方法名+参数作为key
         */
        DEFAULT,
        
        /**
         * 使用IP作为key
         */
        IP,
        
        /**
         * 使用用户ID作为key
         */
        USER
    }
}

// 限流异常
public class RateLimitException extends RuntimeException {
    public RateLimitException(String message) {
        super(message);
    }
}

3. 限流切面实现

// 限流切面
@Aspect
@Component
@Slf4j
public class RateLimitAspect {

    @Autowired
    private SlidingWindowCounter slidingWindowCounter;

    @Autowired
    private HttpServletRequest request;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        
        // 生成限流key
        String key = generateKey(joinPoint, rateLimit);
        
        // 检查是否允许请求
        boolean allowed = slidingWindowCounter.isAllowedAtomic(key, rateLimit.window(), rateLimit.count());
        
        if (!allowed) {
            log.warn("接口访问被限流: key={}, method={}", key, joinPoint.getSignature().getName());
            throw new RateLimitException(rateLimit.message());
        }
        
        // 继续执行原方法
        return joinPoint.proceed();
    }

    /**
     * 生成限流key
     */
    private String generateKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
        String key = rateLimit.key();
        
        if (StringUtils.hasText(key)) {
            // 如果指定了key,使用SpEL解析
            EvaluationContext context = new StandardEvaluationContext();
            Object[] args = joinPoint.getArgs();
            for (int i = 0; i < args.length; i++) {
                context.setVariable("arg" + i, args[i]);
            }
            ExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(key);
            return expression.getValue(context, String.class);
        }
        
        // 根据限流类型生成key
        switch (rateLimit.limitType()) {
            case IP:
                return getClientIp() + ":" + joinPoint.getSignature().getName();
            case USER:
                // 从SecurityContext或请求头中获取用户ID
                String userId = getCurrentUserId();
                return StringUtils.hasText(userId) ? userId : "anonymous";
            case DEFAULT:
            default:
                // 使用方法名作为key
                return joinPoint.getTarget().getClass().getSimpleName() + 
                       "." + joinPoint.getSignature().getName();
        }
    }

    /**
     * 获取客户端IP地址
     */
    private String getClientIp() {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (StringUtils.hasText(xForwardedFor) && !xForwardedFor.contains("unknown")) {
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeader("X-Real-IP");
        if (StringUtils.hasText(xRealIp) && !xRealIp.contains("unknown")) {
            return xRealIp.trim();
        }
        
        return request.getRemoteAddr();
    }

    /**
     * 获取当前用户ID(这里简化处理,实际项目中可能需要从SecurityContext获取)
     */
    private String getCurrentUserId() {
        // 这里只是一个示例,实际项目中可能需要从认证信息中获取用户ID
        // 比如从Spring Security的Authentication对象中获取
        return "user123"; // 示例值
    }
}

关键实现细节

1. 接口应用示例

@RestController
@RequestMapping("/api")
@Slf4j
public class SecurityController {

    @Autowired
    private SmsService smsService;

    @Autowired
    private UserService userService;

    @Autowired
    private PaymentService paymentService;

    /**
     * 发送短信验证码 - 限制每分钟最多5次
     */
    @PostMapping("/sms/send")
    @RateLimit(key = "'sms:' + #phone", window = 60000, count = 5, message = "短信发送过于频繁,请稍后再试")
    public ResponseEntity<ApiResponse<String>> sendSms(@RequestParam String phone) {
        try {
            log.info("发送短信验证码: phone={}", phone);
            
            boolean success = smsService.sendVerificationCode(phone);
            
            if (success) {
                return ResponseEntity.ok(ApiResponse.success("验证码已发送", "发送成功"));
            } else {
                return ResponseEntity.badRequest().body(ApiResponse.error("验证码发送失败"));
            }
        } catch (RateLimitException e) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                    .body(ApiResponse.error(e.getMessage()));
        }
    }

    /**
     * 用户登录 - 限制每分钟最多10次
     */
    @PostMapping("/auth/login")
    @RateLimit(key = "'login:' + #request.ipAddress", limitType = RateLimit.LimitType.IP, 
               window = 60000, count = 10, message = "登录尝试过于频繁,请稍后再试")
    public ResponseEntity<ApiResponse<String>> login(@RequestBody LoginRequest request) {
        try {
            log.info("用户登录请求: username={}", request.getUsername());
            
            String token = userService.login(request);
            
            return ResponseEntity.ok(ApiResponse.success(token, "登录成功"));
        } catch (RateLimitException e) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                    .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            log.error("登录异常", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(ApiResponse.error("登录失败"));
        }
    }

    /**
     * 支付接口 - 限制每小时最多20次
     */
    @PostMapping("/payment/create")
    @RateLimit(key = "'payment:' + #request.userId", limitType = RateLimit.LimitType.USER,
               window = 3600000, count = 20, message = "支付请求过于频繁,请稍后再试")
    public ResponseEntity<ApiResponse<String>> createPayment(@RequestBody PaymentRequest request) {
        try {
            log.info("创建支付订单: userId={}, amount={}", request.getUserId(), request.getAmount());
            
            String orderId = paymentService.createPayment(request);
            
            return ResponseEntity.ok(ApiResponse.success(orderId, "支付订单创建成功"));
        } catch (RateLimitException e) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                    .body(ApiResponse.error(e.getMessage()));
        } catch (Exception e) {
            log.error("创建支付订单异常", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(ApiResponse.error("支付订单创建失败"));
        }
    }

    /**
     * 高频接口示例 - 限制每秒最多100次
     */
    @GetMapping("/health")
    @RateLimit(key = "'health_check'", window = 1000, count = 100, message = "健康检查请求过多")
    public ResponseEntity<ApiResponse<String>> healthCheck() {
        return ResponseEntity.ok(ApiResponse.success("OK", "服务正常"));
    }
}

// 登录请求对象
@Data
public class LoginRequest {
    private String username;
    private String password;
    private String ipAddress; // 客户端IP
}

// 支付请求对象
@Data
public class PaymentRequest {
    private String userId;
    private BigDecimal amount;
    private String productId;
}

2. 配置管理

# application.yml
rate-limit:
  # 全局默认配置
  default:
    window: 60000  # 默认时间窗口1分钟
    count: 10      # 默认最大请求数10次
  # 特定接口配置
  specific:
    sms:
      window: 60000
      count: 5
    login:
      window: 60000
      count: 10
    payment:
      window: 3600000
      count: 20
  # 是否启用限流
  enabled: true
  # 是否记录限流日志
  log-enabled: true

# Redis配置
spring:
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 1000ms
// 限流配置类
@Configuration
@ConfigurationProperties(prefix = "rate-limit")
@Data
public class RateLimitProperties {
    
    private boolean enabled = true;
    private boolean logEnabled = true;
    private DefaultConfig defaultConfig = new DefaultConfig();
    private SpecificConfig specificConfig = new SpecificConfig();
    
    @Data
    public static class DefaultConfig {
        private long window = 60000; // 1分钟
        private int count = 10;
    }
    
    @Data
    public static class SpecificConfig {
        private InterfaceConfig sms = new InterfaceConfig();
        private InterfaceConfig login = new InterfaceConfig();
        private InterfaceConfig payment = new InterfaceConfig();
    }
    
    @Data
    public static class InterfaceConfig {
        private long window = 60000;
        private int count = 10;
    }
}

// 限流配置注入
@Component
public class RateLimitConfig {
    
    @Autowired
    private RateLimitProperties properties;
    
    public boolean isEnabled() {
        return properties.isEnabled();
    }
    
    public boolean isLogEnabled() {
        return properties.isLogEnabled();
    }
    
    public long getDefaultWindow() {
        return properties.getDefaultConfig().getWindow();
    }
    
    public int getDefaultCount() {
        return properties.getDefaultConfig().getCount();
    }
}

3. 监控和统计

// 限流监控服务
@Service
@Slf4j
public class RateLimitMonitor {

    @Autowired
    private SlidingWindowCounter slidingWindowCounter;

    @Autowired
    private MeterRegistry meterRegistry;

    // 限流统计指标
    private final Counter rateLimitedCounter;
    private final Gauge activeRequestsGauge;

    public RateLimitMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.rateLimitedCounter = Counter.builder("rate_limit_requests_total")
            .description("被限流的请求数")
            .register(meterRegistry);
        this.activeRequestsGauge = Gauge.builder("active_rate_limit_keys")
            .description("活跃的限流KEY数量")
            .register(meterRegistry, this, RateLimitMonitor::getActiveKeyCount);
    }

    /**
     * 记录限流事件
     */
    public void recordRateLimitEvent(String key, String endpoint) {
        if (meterRegistry != null) {
            rateLimitedCounter.increment(
                Tags.of("key", key, "endpoint", endpoint)
            );
        }
        log.info("限流事件: key={}, endpoint={}", key, endpoint);
    }

    /**
     * 获取活跃的限流KEY数量(示例实现,实际可能需要从Redis中统计)
     */
    public double getActiveKeyCount() {
        // 这里只是一个示例,实际实现可能需要查询Redis中的key数量
        return 0;
    }

    /**
     * 获取指定key的当前请求数
     */
    public long getCurrentRequestCount(String key, long windowMs) {
        return slidingWindowCounter.getCurrentCount(key, windowMs);
    }

    /**
     * 获取限流统计信息
     */
    public RateLimitStats getStats(String key, long windowMs) {
        long currentCount = getCurrentRequestCount(key, windowMs);
        return RateLimitStats.builder()
            .key(key)
            .currentCount(currentCount)
            .windowMs(windowMs)
            .allowed(currentCount < getMaxAllowedCount(key))
            .build();
    }

    private int getMaxAllowedCount(String key) {
        // 根据key的类型返回对应的最大允许次数
        if (key.startsWith("sms:")) {
            return 5; // 短信限制
        } else if (key.startsWith("login:")) {
            return 10; // 登录限制
        } else if (key.startsWith("payment:")) {
            return 20; // 支付限制
        }
        return 10; // 默认限制
    }
}

// 限流统计信息
@Data
@Builder
public class RateLimitStats {
    private String key;
    private long currentCount;
    private long windowMs;
    private boolean allowed;
    private long remainingCount;
    private long resetTime;
}

业务场景应用

1. 登录接口防刷

@Service
@Slf4j
public class LoginProtectionService {

    @Autowired
    private SlidingWindowCounter slidingWindowCounter;

    @Autowired
    private RateLimitMonitor rateLimitMonitor;

    /**
     * 检查登录尝试频率
     */
    public boolean checkLoginFrequency(String ip, String username) {
        // IP维度的登录频率限制:1分钟内最多10次
        String ipKey = "login_ip:" + ip;
        boolean ipAllowed = slidingWindowCounter.isAllowed(ipKey, 60000, 10);
        
        if (!ipAllowed) {
            rateLimitMonitor.recordRateLimitEvent(ipKey, "login-by-ip");
            return false;
        }

        // 用户维度的登录频率限制:1小时内最多20次
        String userKey = "login_user:" + username;
        boolean userAllowed = slidingWindowCounter.isAllowed(userKey, 3600000, 20);
        
        if (!userAllowed) {
            rateLimitMonitor.recordRateLimitEvent(userKey, "login-by-user");
            return false;
        }

        return true;
    }

    /**
     * 记录登录尝试
     */
    public void recordLoginAttempt(String ip, String username, boolean success) {
        // 记录登录尝试,无论成功与否
        String ipKey = "login_ip:" + ip;
        String userKey = "login_user:" + username;
        
        // 添加到滑动窗口(即使成功也记录,用于统计)
        slidingWindowCounter.isAllowedAtomic(ipKey, 60000, 10);
        slidingWindowCounter.isAllowedAtomic(userKey, 3600000, 20);
        
        if (!success) {
            log.warn("登录失败记录: ip={}, username={}", ip, username);
        }
    }
}

2. 短信接口防刷

@Service
@Slf4j
public class SmsProtectionService {

    @Autowired
    private SlidingWindowCounter slidingWindowCounter;

    @Autowired
    private RateLimitMonitor rateLimitMonitor;

    /**
     * 检查短信发送频率
     */
    public boolean checkSmsFrequency(String phone, String ip) {
        // 手机号码维度的限制:1分钟内最多3次
        String phoneKey = "sms_phone:" + phone;
        boolean phoneAllowed = slidingWindowCounter.isAllowed(phoneKey, 60000, 3);
        
        if (!phoneAllowed) {
            rateLimitMonitor.recordRateLimitEvent(phoneKey, "sms-by-phone");
            return false;
        }

        // IP维度的限制:1分钟内最多5次
        String ipKey = "sms_ip:" + ip;
        boolean ipAllowed = slidingWindowCounter.isAllowed(ipKey, 60000, 5);
        
        if (!ipAllowed) {
            rateLimitMonitor.recordRateLimitEvent(ipKey, "sms-by-ip");
            return false;
        }

        return true;
    }

    /**
     * 记录短信发送
     */
    public void recordSmsSent(String phone, String ip) {
        String phoneKey = "sms_phone:" + phone;
        String ipKey = "sms_ip:" + ip;
        
        slidingWindowCounter.isAllowedAtomic(phoneKey, 60000, 3);
        slidingWindowCounter.isAllowedAtomic(ipKey, 60000, 5);
        
        log.info("短信发送记录: phone={}", phone);
    }
}

3. 支付接口防刷

@Service
@Slf4j
public class PaymentProtectionService {

    @Autowired
    private SlidingWindowCounter slidingWindowCounter;

    @Autowired
    private RateLimitMonitor rateLimitMonitor;

    /**
     * 检查支付请求频率
     */
    public boolean checkPaymentFrequency(String userId, String ip) {
        // 用户维度的限制:1小时内最多20次
        String userKey = "payment_user:" + userId;
        boolean userAllowed = slidingWindowCounter.isAllowed(userKey, 3600000, 20);
        
        if (!userAllowed) {
            rateLimitMonitor.recordRateLimitEvent(userKey, "payment-by-user");
            return false;
        }

        // IP维度的限制:1小时内最多30次
        String ipKey = "payment_ip:" + ip;
        boolean ipAllowed = slidingWindowCounter.isAllowed(ipKey, 3600000, 30);
        
        if (!ipAllowed) {
            rateLimitMonitor.recordRateLimitEvent(ipKey, "payment-by-ip");
            return false;
        }

        return true;
    }

    /**
     * 记录支付请求
     */
    public void recordPaymentRequest(String userId, String ip) {
        String userKey = "payment_user:" + userId;
        String ipKey = "payment_ip:" + ip;
        
        slidingWindowCounter.isAllowedAtomic(userKey, 3600000, 20);
        slidingWindowCounter.isAllowedAtomic(ipKey, 3600000, 30);
        
        log.info("支付请求记录: userId={}", userId);
    }
}

最佳实践建议

1. 参数配置建议

// 限流参数配置建议
public class RateLimitConstants {
    
    // 短信接口
    public static final long SMS_WINDOW_MS = 60000;        // 1分钟
    public static final int SMS_COUNT_PER_WINDOW = 3;      // 最多3次
    
    // 登录接口
    public static final long LOGIN_WINDOW_MS = 60000;      // 1分钟
    public static final int LOGIN_COUNT_PER_WINDOW = 10;   // 最多10次
    
    // 支付接口
    public static final long PAYMENT_WINDOW_MS = 3600000;  // 1小时
    public static final int PAYMENT_COUNT_PER_WINDOW = 20; // 最多20次
    
    // 健康检查接口
    public static final long HEALTH_WINDOW_MS = 1000;      // 1秒
    public static final int HEALTH_COUNT_PER_WINDOW = 100; // 最多100次
}

2. 性能优化

// 限流服务优化版本
@Service
@Slf4j
public class OptimizedRateLimitService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 使用连接池和批量操作优化性能
    private final LoadingCache<String, Boolean> localCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(Duration.ofSeconds(10))
        .build(this::checkRateLimit);

    /**
     * 带本地缓存的限流检查
     * 适用于高频但不需要精确控制的场景
     */
    public boolean isAllowedWithCache(String key, long windowMs, int maxCount) {
        String cacheKey = key + ":" + windowMs + ":" + maxCount;
        return localCache.get(cacheKey);
    }

    private Boolean checkRateLimit(String cacheKey) {
        // 解析cacheKey获取实际参数
        String[] parts = cacheKey.split(":");
        String key = parts[0];
        long windowMs = Long.parseLong(parts[1]);
        int maxCount = Integer.parseInt(parts[2]);

        return isAllowedInternal(key, windowMs, maxCount);
    }

    private boolean isAllowedInternal(String key, long windowMs, int maxCount) {
        // 实际的限流检查逻辑
        return new SlidingWindowCounter().isAllowedAtomic(key, windowMs, maxCount);
    }
}

3. 监控告警

// 限流告警服务
@Component
@Slf4j
public class RateLimitAlertService {

    @Autowired
    private MeterRegistry meterRegistry;

    /**
     * 检查是否需要告警
     */
    @Scheduled(fixedRate = 30000) // 每30秒检查一次
    public void checkRateLimitAlerts() {
        // 获取限流统计数据
        Double rateLimitedCount = meterRegistry.counter("rate_limit_requests_total")
            .count();

        // 如果限流次数异常增多,发送告警
        if (rateLimitedCount != null && rateLimitedCount > 1000) { // 假设阈值为1000
            log.warn("检测到大量限流请求: count={}", rateLimitedCount);
            // 发送告警通知(如钉钉、邮件等)
            sendAlert("检测到大量限流请求,请检查是否有恶意攻击");
        }
    }

    private void sendAlert(String message) {
        // 实现告警发送逻辑
        log.error("限流告警: {}", message);
    }
}

预期效果

通过这套滑动窗口计数防刷方案,我们可以实现:

  • 精准控制:精确控制任意时间窗口内的请求频率
  • 资源保护:有效保护服务器资源不被恶意请求消耗
  • 成本控制:减少因恶意刷取造成的业务成本
  • 用户体验:保障正常用户的访问体验
  • 监控完善:全面的限流监控和告警机制

这套方案让系统从"被动防御"变成了"主动防护",大大提升了系统的安全性和稳定性。


欢迎关注公众号"服务端技术精选",获取更多技术干货!


标题:SpringBoot + 接口防刷 + 滑动窗口计数:登录、短信、支付接口防暴力攻击
作者:jiangyi
地址:http://jiangyi.space/articles/2026/02/14/1770877410867.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消