接口被恶意狂刷,如何处理

引言:接口防护的重要性

某个接口突然被大量请求轰炸,导致系统响应缓慢甚至崩溃?或者用户恶意刷接口获取优惠券、红包等福利?再或者爬虫程序疯狂调用接口抓取数据,严重影响正常用户使用?

这就是接口防护的典型难题。今天我们就来聊聊如何构建一个完整的接口防护体系,让你的系统远离恶意刷接口的困扰。

接口被刷的常见场景

1. 登录接口被暴力破解

攻击者使用字典攻击,尝试大量用户名密码组合。

2. 短信验证码接口被刷

恶意用户反复请求验证码,导致短信费用飙升。

3. 优惠券接口被刷

利用漏洞大量领取优惠券,造成经济损失。

4. 商品抢购接口被刷

使用脚本抢购限量商品,影响公平性。

5. API接口被爬虫攻击

大量请求抓取数据,影响正常业务。

防护策略总览

1. 限流策略

  • 固定窗口限流:单位时间内限制请求数
  • 滑动窗口限流:更精确的时间窗口
  • 令牌桶限流:平滑的请求处理
  • 漏桶限流:恒定的处理速率

2. 认证策略

  • 图形验证码:人机验证
  • 滑块验证:行为验证
  • 短信验证:手机验证
  • 生物识别:指纹、人脸等

3. 黑白名单

  • IP黑名单:阻止恶意IP访问
  • 用户黑名单:阻止恶意用户
  • 设备黑名单:阻止恶意设备

技术实现方案

1. 基于Redis的限流实现

@Component
public class RateLimiterService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 固定窗口限流
     */
    public boolean isAllowed(String key, int limit, int windowSeconds) {
        String luaScript = 
            "local key = KEYS[1]\n" +
            "local limit = tonumber(ARGV[1])\n" +
            "local window = tonumber(ARGV[2])\n" +
            "local current = redis.call('GET', key)\n" +
            "if current == false then\n" +
            "    redis.call('SET', key, 1)\n" +
            "    redis.call('EXPIRE', key, window)\n" +
            "    return 1\n" +
            "end\n" +
            "current = tonumber(current)\n" +
            "if current < limit then\n" +
            "    redis.call('INCR', key)\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        
        RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = (Long) redisTemplate.execute(script, 
            Arrays.asList(key), String.valueOf(limit), String.valueOf(windowSeconds));
        
        return result == 1;
    }
    
    /**
     * 滑动窗口限流
     */
    public boolean isAllowedSlidingWindow(String key, int limit, int windowSeconds) {
        String luaScript = 
            "local key = KEYS[1]\n" +
            "local limit = tonumber(ARGV[1])\n" +
            "local window = tonumber(ARGV[2])\n" +
            "local current = redis.call('TIME')[1]\n" +
            "local pipeline = redis.call('ZRANGEBYSCORE', key, current - window, current)\n" +
            "if #pipeline >= limit then\n" +
            "    return 0\n" +
            "end\n" +
            "redis.call('ZADD', key, current, current .. math.random(1000000))\n" +
            "redis.call('ZREMRANGEBYSCORE', key, 0, current - window)\n" +
            "redis.call('EXPIRE', key, window)\n" +
            "return 1";
        
        RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = (Long) redisTemplate.execute(script, 
            Arrays.asList(key), String.valueOf(limit), String.valueOf(windowSeconds));
        
        return result == 1;
    }
}

2. 自定义限流注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String key() default ""; // 限流键
    int limit() default 10; // 限制次数
    int window() default 60; // 时间窗口(秒)
    String message() default "请求过于频繁,请稍后再试"; // 提示信息
}

3. 限流拦截器

@Aspect
@Component
public class RateLimitAspect {
    
    @Autowired
    private RateLimiterService rateLimiterService;
    
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
        // 构建限流键
        String key = buildRateLimitKey(point, rateLimit);
        
        // 检查是否允许访问
        boolean allowed = rateLimiterService.isAllowed(key, rateLimit.limit(), rateLimit.window());
        
        if (!allowed) {
            throw new RateLimitException(rateLimit.message());
        }
        
        return point.proceed();
    }
    
    private String buildRateLimitKey(ProceedingJoinPoint point, RateLimit rateLimit) {
        String key = rateLimit.key();
        if (StringUtils.isEmpty(key)) {
            // 默认使用方法名
            MethodSignature signature = (MethodSignature) point.getSignature();
            key = signature.getMethod().getName();
        }
        
        // 添加用户标识
        HttpServletRequest request = getCurrentRequest();
        String userId = getCurrentUserId(request);
        if (userId != null) {
            key = "rate_limit:" + key + ":" + userId;
        } else {
            // 使用IP地址
            String ip = getIpAddress(request);
            key = "rate_limit:" + key + ":" + ip;
        }
        
        return key;
    }
    
    private HttpServletRequest getCurrentRequest() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return ((ServletRequestAttributes) attributes).getRequest();
    }
    
    private String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

4. IP黑名单管理

@Service
public class IpBlacklistService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 检查IP是否在黑名单中
     */
    public boolean isBlacklisted(String ip) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForSet().isMember("ip:blacklist", ip));
    }
    
    /**
     * 添加IP到黑名单
     */
    public void addToBlacklist(String ip, String reason) {
        redisTemplate.opsForSet().add("ip:blacklist", ip);
        // 记录封禁原因
        redisTemplate.opsForHash().put("ip:blacklist:reason", ip, reason);
    }
    
    /**
     * 从黑名单移除
     */
    public void removeFromBlacklist(String ip) {
        redisTemplate.opsForSet().remove("ip:blacklist", ip);
        redisTemplate.opsForHash().delete("ip:blacklist:reason", ip);
    }
    
    /**
     * 自动封禁策略
     */
    public void autoBlacklist(String ip, String action) {
        String key = "attack:count:" + ip + ":" + action;
        
        // 增加攻击计数
        Long count = redisTemplate.opsForValue().increment(key);
        
        if (count != null && count >= 10) { // 10次攻击后封禁
            addToBlacklist(ip, "自动封禁: " + action + "攻击");
            // 设置封禁时长
            redisTemplate.expire(key, Duration.ofHours(24));
        } else {
            // 设置计数器过期时间
            redisTemplate.expire(key, Duration.ofMinutes(10));
        }
    }
}

5. 图形验证码

@RestController
public class CaptchaController {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @GetMapping("/captcha")
    public ResponseEntity<CaptchaResponse> generateCaptcha() {
        // 生成验证码
        String code = generateRandomCode(4);
        String captchaId = UUID.randomUUID().toString();
        
        // 存储验证码到Redis
        redisTemplate.opsForValue().set("captcha:" + captchaId, code, Duration.ofMinutes(5));
        
        // 生成验证码图片
        BufferedImage image = createCaptchaImage(code);
        
        // 返回验证码ID和图片
        CaptchaResponse response = new CaptchaResponse();
        response.setCaptchaId(captchaId);
        response.setImageData(imageToBase64(image));
        
        return ResponseEntity.ok(response);
    }
    
    @PostMapping("/captcha/verify")
    public ResponseEntity<Boolean> verifyCaptcha(@RequestBody CaptchaVerifyRequest request) {
        String storedCode = (String) redisTemplate.opsForValue().get("captcha:" + request.getCaptchaId());
        
        if (storedCode != null && storedCode.equalsIgnoreCase(request.getCode())) {
            // 验证成功,删除验证码
            redisTemplate.delete("captcha:" + request.getCaptchaId());
            return ResponseEntity.ok(true);
        }
        
        return ResponseEntity.ok(false);
    }
    
    private String generateRandomCode(int length) {
        String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        
        for (int i = 0; i < length; i++) {
            sb.append(chars.charAt(random.nextInt(chars.length())));
        }
        
        return sb.toString();
    }
    
    private BufferedImage createCaptchaImage(String code) {
        int width = 120;
        int height = 40;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();
        
        // 设置背景色
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);
        
        // 设置字体
        g.setFont(new Font("Arial", Font.BOLD, 24));
        
        // 绘制验证码
        for (int i = 0; i < code.length(); i++) {
            g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
            g.drawString(String.valueOf(code.charAt(i)), 20 + i * 20, 25);
        }
        
        // 添加干扰线
        for (int i = 0; i < 5; i++) {
            g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
            g.drawLine(random.nextInt(width), random.nextInt(height), 
                      random.nextInt(width), random.nextInt(height));
        }
        
        g.dispose();
        return image;
    }
    
    private String imageToBase64(BufferedImage image) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(image, "png", baos);
            byte[] imageBytes = baos.toByteArray();
            return Base64.getEncoder().encodeToString(imageBytes);
        } catch (IOException e) {
            throw new RuntimeException("验证码图片转换失败", e);
        }
    }
}

6. 全局防护拦截器

@Component
public class GlobalProtectionInterceptor implements HandlerInterceptor {
    
    @Autowired
    private IpBlacklistService ipBlacklistService;
    
    @Autowired
    private RateLimiterService rateLimiterService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        String ip = getIpAddress(request);
        
        // 检查IP是否在黑名单中
        if (ipBlacklistService.isBlacklisted(ip)) {
            response.setStatus(403);
            response.getWriter().write("您的IP已被封禁");
            return false;
        }
        
        // 检查全局限流
        String globalKey = "global:rate_limit:" + ip;
        if (!rateLimiterService.isAllowed(globalKey, 1000, 60)) { // 每分钟最多1000次请求
            response.setStatus(429);
            response.getWriter().write("请求过于频繁");
            return false;
        }
        
        return true;
    }
    
    private String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

高级防护策略

1. 行为分析

@Service
public class BehaviorAnalysisService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 分析用户行为
     */
    public boolean analyzeBehavior(String userId, String action, HttpServletRequest request) {
        String ip = getIpAddress(request);
        
        // 记录用户行为
        String behaviorKey = "behavior:" + userId + ":" + action;
        redisTemplate.opsForValue().increment(behaviorKey);
        redisTemplate.expire(behaviorKey, Duration.ofMinutes(5));
        
        // 记录IP行为
        String ipBehaviorKey = "ip_behavior:" + ip + ":" + action;
        redisTemplate.opsForValue().increment(ipBehaviorKey);
        redisTemplate.expire(ipBehaviorKey, Duration.ofMinutes(5));
        
        // 检查异常行为
        Long userCount = redisTemplate.opsForValue().get(behaviorKey);
        Long ipCount = redisTemplate.opsForValue().get(ipBehaviorKey);
        
        // 如果用户在短时间内进行大量操作,可能是异常行为
        if (userCount != null && userCount > 50) {
            // 记录异常行为
            recordAnomaly(userId, action, "用户行为异常", request);
            return false;
        }
        
        // 如果IP在短时间内进行大量操作,可能是爬虫
        if (ipCount != null && ipCount > 100) {
            // 记录异常行为并加入IP黑名单
            recordAnomaly(ip, action, "IP行为异常", request);
            ipBlacklistService.addToBlacklist(ip, "IP行为异常");
            return false;
        }
        
        return true;
    }
    
    private void recordAnomaly(String target, String action, String reason, HttpServletRequest request) {
        AnomalyRecord record = AnomalyRecord.builder()
            .target(target)
            .action(action)
            .reason(reason)
            .ip(getIpAddress(request))
            .timestamp(new Date())
            .build();
        
        // 存储异常记录
        String key = "anomaly:" + System.currentTimeMillis();
        redisTemplate.opsForValue().set(key, record, Duration.ofDays(7));
    }
}

2. 设备指纹识别

@Service
public class DeviceFingerprintService {
    
    /**
     * 生成设备指纹
     */
    public String generateFingerprint(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        
        // 获取请求头信息
        sb.append(request.getHeader("User-Agent"));
        sb.append(request.getHeader("Accept-Language"));
        sb.append(request.getHeader("Accept-Encoding"));
        sb.append(request.getHeader("Accept"));
        
        // 获取其他信息
        sb.append(request.getRemoteAddr());
        sb.append(request.getHeader("X-Forwarded-For"));
        
        // 计算MD5
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
    }
    
    /**
     * 检查设备是否异常
     */
    public boolean isDeviceNormal(String fingerprint, String action) {
        String key = "device:" + fingerprint + ":" + action;
        Long count = redisTemplate.opsForValue().increment(key);
        
        if (count != null && count > 100) { // 同一设备同一操作超过100次
            return false;
        }
        
        // 设置过期时间
        redisTemplate.expire(key, Duration.ofHours(1));
        return true;
    }
}

监控与告警

1. 防护指标监控

@Component
public class ProtectionMetricsCollector {
    
    private final MeterRegistry meterRegistry;
    
    public void recordRateLimitHit(String endpoint) {
        Counter.builder("rate_limit_hits_total")
            .tag("endpoint", endpoint)
            .register(meterRegistry)
            .increment();
    }
    
    public void recordBlacklistHit(String ip) {
        Counter.builder("blacklist_hits_total")
            .tag("ip", ip)
            .register(meterRegistry)
            .increment();
    }
    
    public void recordAnomaly(String type, String target) {
        Counter.builder("anomaly_detected_total")
            .tag("type", type)
            .tag("target", target)
            .register(meterRegistry)
            .increment();
    }
}

2. 异常告警

@Component
public class ProtectionAlertService {
    
    /**
     * 检查是否需要告警
     */
    public void checkAndAlert() {
        // 检查限流命中率
        Long rateLimitHits = getRateLimitHitsInLastMinute();
        if (rateLimitHits > 1000) { // 一分钟内限流命中超过1000次
            sendAlert("限流告警", "一分钟内限流命中超过1000次,请检查系统");
        }
        
        // 检查黑名单命中率
        Long blacklistHits = getBlacklistHitsInLastMinute();
        if (blacklistHits > 100) { // 一分钟内黑名单命中超过100次
            sendAlert("黑名单告警", "一分钟内黑名单命中超过100次,请检查攻击情况");
        }
        
        // 检查异常行为
        Long anomalyCount = getAnomalyCountInLastHour();
        if (anomalyCount > 50) { // 一小时内异常行为超过50次
            sendAlert("异常行为告警", "一小时内检测到超过50次异常行为");
        }
    }
    
    private void sendAlert(String title, String message) {
        // 发送告警通知(邮件、短信、钉钉等)
        // 实现告警发送逻辑
    }
}

最佳实践

1. 分层防护策略

  • 第一层:全局限流,防止大规模攻击
  • 第二层:接口限流,保护关键接口
  • 第三层:认证验证,确保用户合法性
  • 第四层:行为分析,识别异常模式

2. 限流参数配置

@Configuration
public class RateLimitConfig {
    
    // 登录接口限流:每分钟最多5次
    @Bean
    @Qualifier("loginRateLimit")
    public RateLimitRule loginRateLimit() {
        return RateLimitRule.builder()
            .limit(5)
            .window(60)
            .build();
    }
    
    // 验证码接口限流:每分钟最多3次
    @Bean
    @Qualifier("captchaRateLimit")
    public RateLimitRule captchaRateLimit() {
        return RateLimitRule.builder()
            .limit(3)
            .window(60)
            .build();
    }
    
    // 普通接口限流:每分钟最多100次
    @Bean
    @Qualifier("normalRateLimit")
    public RateLimitRule normalRateLimit() {
        return RateLimitRule.builder()
            .limit(100)
            .window(60)
            .build();
    }
}

3. 动态调整策略

@Service
public class DynamicProtectionService {
    
    /**
     * 根据系统负载动态调整防护策略
     */
    public void adjustProtectionStrategy() {
        // 获取系统负载信息
        double systemLoad = getSystemLoad();
        
        if (systemLoad > 0.8) { // 系统负载过高
            // 收紧限流策略
            tightenRateLimit();
        } else if (systemLoad < 0.3) { // 系统负载较低
            // 放宽限流策略
            loosenRateLimit();
        }
    }
    
    private void tightenRateLimit() {
        // 收紧限流参数
        // 实现逻辑...
    }
    
    private void loosenRateLimit() {
        // 放宽限流参数
        // 实现逻辑...
    }
}

总结

接口防护是一个系统工程,需要从多个维度进行考虑:

  1. 预防为主:建立完善的限流机制
  2. 检测及时:实时监控异常行为
  3. 响应迅速:快速封禁恶意请求
  4. 持续优化:根据攻击模式调整策略

记住,防护不是一成不变的,需要根据业务特点和攻击模式持续优化。掌握了这些技巧,你就能构建一个坚固的防护体系,让恶意刷接口的行为无处遁形!


标题:接口被恶意狂刷,如何处理
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/05/1767613250670.html

    0 评论
avatar