1. 接口防刷的必要性与挑战
在当今互联网应用中,接口暴力攻击已成为系统安全的主要威胁之一。作为一名长期从事后端开发的工程师,我见过太多因为没有做好接口防护而导致的生产事故。去年我们团队就处理过一个典型案例:某电商平台的短信验证码接口被恶意刷取,短短两小时内产生了近5万元的短信费用,同时还导致正常用户无法收到验证码。
1.1 接口暴力攻击的四大危害
服务器资源枯竭是最直接的冲击。恶意请求会像洪水一样涌入:
- 每个请求至少占用1个线程,Tomcat默认200线程池瞬间爆满
- 数据库连接池被占满(如HikariCP默认10连接)
- 一次简单的查询在高压下可能消耗10MB以上内存
- 1Gbps带宽的服务器,每秒约10万次请求就能打满
业务成本失控更令人头疼:
- 短信接口被刷:每条短信成本0.03-0.1元,1万次就是300-1000元
- 支付接口被刷:可能产生大量小额测试交易,触发风控警报
- 第三方API调用:如地图服务,超额部分可能按10倍计费
用户体验崩塌的连锁反应:
- 正常用户请求响应时间从200ms飙升到5s+
- 关键业务接口返回429 Too Many Requests
- 移动端APP出现大面积白屏或卡死
数据安全风险最为致命:
- 暴力破解尝试:6位数字密码理论上100万次必中
- 优惠券/积分被刷:某平台曾一夜被薅走200万积分
- 数据泄露:爬虫通过API批量获取用户隐私信息
1.2 传统防护方案的局限性
早期我们尝试过几种常规方案,但都存在明显缺陷:
固定窗口计数器(如1分钟100次):
java复制// 伪代码示例
if(redis.incr(key) > 100){
throw new RateLimitException();
}
问题在于时间窗口边界会出现请求突增:
- 窗口切换瞬间可能允许200次请求(前1秒和后1秒)
令牌桶算法:
java复制// 伪代码示例
if(tokenBucket.tryAcquire()){
// 通过
}else{
// 限流
}
虽然平滑但实现复杂,且难以精确控制瞬时流量
简单IP黑名单:
java复制if(blacklist.contains(ip)){
return false;
}
容易被绕过(代理IP池),且可能误伤公共出口IP
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 滑动窗口计数方案设计
经过多次迭代,我们最终采用了基于Redis ZSET的滑动窗口方案。这个设计的精妙之处在于,它既保持了固定窗口的简单性,又解决了边界问题,还能实现毫秒级精度控制。
2.1 核心数据结构设计
使用Redis的ZSET(有序集合)存储请求记录:
- member:请求唯一标识(UUID或雪花ID)
- score:请求时间戳(毫秒精度)
关键操作示例:
java复制// 添加请求记录
redis.zadd(key, System.currentTimeMillis(), requestId);
// 统计窗口内请求数
long count = redis.zcount(key, currentTime - windowSize, currentTime);
2.2 滑动窗口算法流程
完整的工作流程分为五个步骤:
-
请求到达时:
- 生成唯一请求ID
- 记录当前时间戳T1
-
清理过期请求:
- 删除ZSET中score小于(T1 - 窗口大小)的记录
- 使用
ZREMRANGEBYSCORE命
