接口防刷设计

11 阅读3分钟

主要目的

保护系统资源、保障用户体验和确保业务安全

存在的场景

  • 爬虫

大量抓取数据,耗尽服务器资源

  • 暴力破解

针对登录接口高频尝试密码

  • CC攻击

应用层DDoS,耗尽业务资源

  • 业务滥用

如秒杀场景下的机器抢单

应对的策略

请求合法性校验

服务端处理请求之前,验证该请求是否来自可信的客户端、是否完整未被篡改、是否在有效期内的一系列措施。 它是接口防刷的第一道防线,用于拦截伪造、重放、篡改的恶意请求

  • API签名校验

    // 客户端生成签名(伪代码) String secret = "your-secret"; String data = "param1=value1&param2=value2"; long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString(); String sign = HmacSHA256(data + timestamp + nonce, secret);

    // 服务端校验拦截器 public boolean preHandle(HttpServletRequest request) { String sign = request.getHeader("X-Sign"); long timestamp = Long.parseLong(request.getHeader("X-Timestamp")); String nonce = request.getHeader("X-Nonce"); String body = getRequestBody(request);

    // 1. 时间戳检查
    if (Math.abs(System.currentTimeMillis() - timestamp) > 5 * 60 * 1000) {
        return false; // 请求过期
    }
    
    // 2. Nonce检查(Redis)
    String nonceKey = "nonce:" + nonce;
    if (redisTemplate.hasKey(nonceKey)) {
        return false; // 重复请求
    }
    redisTemplate.opsForValue().set(nonceKey, "1", 5, TimeUnit.MINUTES);
    
    // 3. 重新计算签名
    String serverSign = HmacSHA256(body + timestamp + nonce, getSecretByAppKey(appKey));
    if (!serverSign.equals(sign)) {
        return false; // 签名错误
    }
    return true;
    

    }

  • 身份认证(JWT校验示例)

    @GetMapping("/user/info") public Result getUserInfo(@RequestHeader("Authorization") String token) { try { // 解析JWT,验证签名和过期时间 Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token.replace("Bearer ", "")) .getBody(); String userId = claims.getSubject(); // 业务处理 } catch (Exception e) { return Result.error(401, "无效token"); } }

限流

  • 基于redis demo

    @Component public class RateLimitInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate;

    // 加载Lua脚本
    private DefaultRedisScript<Long> redisScript;
    
    @PostConstruct
    public void init() {
        redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ratelimit.lua")));
        redisScript.setResultType(Long.class);
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String ip = getClientIp(request);
        String key = "rate:ip:" + ip;
        long max = 10;        // 10次
        long period = 60;     // 60秒
        
        Long result = redisTemplate.execute(redisScript, Arrays.asList(key), String.valueOf(max), String.valueOf(period));
        if (result == null || result == 0) {
            response.setStatus(429);
            response.getWriter().write("Too Many Requests");
            return false;
        }
        return true;
    }
    
    private String getClientIp(HttpServletRequest request) {
        // 从x-forwarded-for等头获取真实IP
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr();
        return ip;
    }
    

    }

验证码机制

验证码(CAPTCHA)是“区分计算机和人类的公开全自动图灵测试”的缩写

  • 防止恶意程序自动提交表单(如注册、登录、评论)

  • 阻止暴力破解密码、刷票、爬虫等自动化攻击

  • 在触发限流策略后,作为二次验证手段,确认操作由真实用户发起

  • 配置Kaptcha(生成图形验证码)

    @Configuration public class KaptchaConfig { @Bean public Producer kaptchaProducer() { Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "150"); properties.setProperty("kaptcha.image.height", "50"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple"); properties.setProperty("kaptcha.background.clear.from", "255,255,255"); properties.setProperty("kaptcha.background.clear.to", "255,255,255"); DefaultKaptcha kaptcha = new DefaultKaptcha(); kaptcha.setConfig(new Config(properties)); return kaptcha; } }

  • 生成验证码接口(Controller)

    @RestController public class CaptchaController { @Autowired private Producer captchaProducer; @Autowired private RedisTemplate<String, String> redisTemplate;

    @GetMapping("/captcha")
    public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 生成验证码文本
        String captchaText = captchaProducer.createText();
        // 生成验证码图片
        BufferedImage image = captchaProducer.createImage(captchaText);
        
        // 存储到Redis(使用唯一标识,如UUID或手机号)
        String captchaKey = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set("captcha:" + captchaKey, captchaText, 5, TimeUnit.MINUTES);
        
        // 将key返回给前端(可通过响应头或JSON)
        response.setHeader("Captcha-Key", captchaKey);
        
        // 输出图片
        response.setContentType("image/jpeg");
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(image, "jpg", out);
        out.close();
    }
    

    }

黑/白名单

在接口防刷体系中,黑/白名单是一种基于来源(IP、用户ID、设备指纹等)的快速过滤机制

  • 名单格式

IP黑/白名单:支持单个IP、IP段(CIDR,如192.168.1.0/24)、通配符(如192.168..)

用户ID黑/白名单:针对已登录用户的UID进行控制

设备指纹:针对移动端设备ID(如IMEI、IDFA)进行封禁

@Component
public class BlackWhiteListInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // IP工具:判断IP是否在某个CIDR网段内
    private boolean ipMatchesCidr(String ip, String cidr) {
        try {
            SubnetUtils subnet = new SubnetUtils(cidr);
            return subnet.getInfo().isInRange(ip);
        } catch (Exception e) {
            return false; // 格式错误视为不匹配
        }
    }

    // 检查是否在白名单中
    private boolean isInWhitelist(String ip) {
        Set<String> whitelist = redisTemplate.opsForSet().members("whitelist:ip");
        if (whitelist == null || whitelist.isEmpty()) return false;
        for (String pattern : whitelist) {
            if (pattern.contains("/")) { // CIDR网段
                if (ipMatchesCidr(ip, pattern)) return true;
            } else if (pattern.contains("*")) { // 通配符简单处理(如192.168.*.*)
                // 可将通配符转换为正则,此处简化
                String regex = pattern.replace(".", "\\.").replace("*", "\\d+");
                if (ip.matches(regex)) return true;
            } else { // 精确IP
                if (pattern.equals(ip)) return true;
            }
        }
        return false;
    }

    // 检查是否在黑名单中
    private boolean isInBlacklist(String ip) {
        // 直接判断Set中是否存在该IP(精确匹配),但IP段需要遍历判断
        Set<String> blacklist = redisTemplate.opsForSet().members("blacklist:ip");
        if (blacklist == null || blacklist.isEmpty()) return false;
        for (String pattern : blacklist) {
            if (pattern.contains("/")) {
                if (ipMatchesCidr(ip, pattern)) return true;
            } else if (pattern.contains("*")) {
                String regex = pattern.replace(".", "\\.").replace("*", "\\d+");
                if (ip.matches(regex)) return true;
            } else {
                if (pattern.equals(ip)) return true;
            }
        }
        return false;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String ip = getClientIp(request);

        // 1. 白名单检查:如果命中,直接放行
        if (isInWhitelist(ip)) {
            return true;
        }

        // 2. 黑名单检查:如果命中,拒绝请求
        if (isInBlacklist(ip)) {
            response.setStatus(403);
            response.getWriter().write("Your IP is blocked due to abnormal behavior.");
            return false;
        }

        // 3. 继续后续拦截器(如限流)
        return true;
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr();
        // 处理多级代理,取第一个非unknown的IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

总结

接口防刷是一个系统工程,需结合多种手段层层设防

  • 基础防御:签名、限流、黑白名单
  • 动态防御:验证码、行为分析
  • 兜底防御:异步队列、熔断降级