1. 项目概述:接口防刷的核心价值
在当今互联网应用中,暴力破解和恶意刷接口已成为最常见的安全威胁之一。我最近为一个电商平台重构了他们的安全防护体系,发现仅登录接口每天就会遭受超过2万次的暴力破解尝试。通过引入滑动窗口计数算法,我们成功将恶意请求拦截率提升到98%,同时保证了正常用户的流畅体验。
这种防护机制特别适用于三类核心业务接口:
- 登录认证接口(防止撞库攻击)
- 短信验证码接口(防止短信轰炸)
- 支付确认接口(防止交易篡改)
关键提示:防刷系统设计必须平衡安全性和用户体验,过于严格的限制可能导致正常用户被误伤。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 SpringBoot的基础配置
我们选择SpringBoot 2.7.x作为基础框架,这是目前企业级应用最稳定的版本。在pom.xml中需要添加以下关键依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
AOP用于实现切面编程,Guava则提供了高性能的本地缓存工具。这里没有选择Redis是因为对于中小规模应用,本地缓存完全够用且延迟更低。
2.2 滑动窗口算法原理
滑动窗口算法是防刷系统的核心,其本质是在时间轴上维护一个动态的计数窗口。与传统固定时间窗口相比,它解决了临界时间点请求突增的问题。
我设计了一个基于环形队列的实现方案:
- 将时间窗口划分为N个时间片(如60秒窗口分为60个1秒片)
- 每个时间片维护独立的计数器
- 窗口滑动时淘汰过期时间片
- 统计当前窗口内所有有效时间片的计数总和
这种实现的时间复杂度是O(1),远优于传统的全量统计方式。实测在8核服务器上可处理10万+ QPS的计数请求。
3. 核心代码实现
3.1 滑动窗口计数器
java复制public class SlidingWindowCounter {
private final int windowSize; // 窗口大小(秒)
private final int[] timeSlices; // 时间片数组
private final AtomicInteger total = new AtomicInteger(0);
private volatile int currentIndex = 0;
public SlidingWindowCounter(int windowSize) {
this.windowSize = windowSize;
this.timeSlices = new int[windowSize];
}
public synchronized void increment() {
// 获取当前时间片索引
int nowIndex = (int)(System.currentTimeMillis()/1000) % windowSize;
// 如果时间片变化,需要重置过期计数器
if(nowIndex != currentIndex) {
int delta = windowSize - (nowIndex - currentIndex + windowSize) % windowSize;
for(int i=1; i<=delta; i++) {
int clearIndex = (currentIndex + i) % windowSize;
total.addAndGet(-timeSlices[clearIndex]);
timeSlices[clearIndex] = 0;
}
currentIndex = nowIndex;
}
timeSlices[currentIndex]++;
total.incrementAndGet();
}
public int getTotal() {
return total.get();
}
}
3.2 AOP切面实现
java复制@Aspect
@Component
public class ApiLimitAspect {
private static final Map<String, SlidingWindowCounter> counterMap = new ConcurrentHashMap<>();
@Pointcut("@annotation(apiLimit)")
public void apiLimitPointcut(ApiLimit apiLimit) {}
@Around("apiLimitPointcut(apiLimit)")
public Object around(ProceedingJoinPoint joinPoint, ApiLimit apiLimit) throws Throwable {
String key = getApiKey(joinPoint);
SlidingWindowCounter counter = counterMap.computeIfAbsent(
key, k -> new SlidingWindowCounter(apiLimit.windowSize()));
if(counter.getTotal() >= apiLimit.maxCount()) {
throw new BusinessException("请求过于频繁,请稍后再试");
}
counter.increment();
return joinPoint.proceed();
}
private String getApiKey(ProceedingJoinPoint joinPoint) {
// 根据IP+接口生成唯一key
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
return request.getRemoteAddr() + ":" +
joinPoint.getSignature().toShortString();
}
}
4. 实战优化技巧
4.1 动态阈值调整
在实际项目中,我们发现固定阈值存在两个问题:
- 业务高峰期容易误伤正常用户
- 攻击者可以试探出阈值边界
解决方案是实现动态阈值算法:
java复制// 基于历史流量自动调整阈值
public int calculateDynamicThreshold(String apiKey) {
// 获取上周同时段的平均请求量
double historyAvg = getHistoryAverage(apiKey);
// 取历史均值的3倍作为阈值,最低不低于10次/分钟
return Math.max((int)(historyAvg * 3), 10);
}
4.2 分级惩罚机制
我们设计了三级惩罚策略:
- 首次超限:返回429状态码,提示"操作过于频繁"
- 连续超限:增加图形验证码
- 恶意攻击:IP加入黑名单30分钟
java复制public void handleOverLimit(String ip) {
int violationCount = redis.incr("limit:violation:" + ip);
if(violationCount > 5) {
redis.setex("blacklist:" + ip, 1800, "1");
} else if(violationCount > 2) {
enableCaptcha(ip);
}
}
5. 性能优化方案
5.1 内存优化技巧
在高并发场景下,我们发现原始实现存在两个性能瓶颈:
- 同步锁竞争激烈
- 内存占用过高
优化后的方案:
- 使用LongAdder替代AtomicInteger
- 采用分片计数减少锁竞争
- 压缩时间片存储结构
java复制// 分片计数器实现
public class ShardedCounter {
private final int shards;
private final LongAdder[] counters;
public ShardedCounter(int shards) {
this.shards = shards;
this.counters = new LongAdder[shards];
for(int i=0; i<shards; i++) {
counters[i] = new LongAdder();
}
}
public void increment() {
int index = ThreadLocalRandom.current().nextInt(shards);
counters[index].increment();
}
public long sum() {
long sum = 0;
for(LongAdder adder : counters) {
sum += adder.sum();
}
return sum;
}
}
5.2 集群扩展方案
当应用需要水平扩展时,本地计数方案需要调整为分布式实现。我们对比了三种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis计数器 | 实现简单 | 网络开销大 | 中小规模集群 |
| Hazelcast | 内存网格 | 学习成本高 | 大规模应用 |
| 本地计数+定期同步 | 性能最好 | 存在短暂不一致 | 最终一致性场景 |
最终选择方案三的实现代码:
java复制@Scheduled(fixedRate = 5000)
public void syncCounterToRedis() {
counterMap.forEach((key, counter) -> {
long current = counter.getTotal();
long lastSynced = redis.get("last_sync:" + key);
if(current > lastSynced) {
redis.incrBy("global_count:" + key, current - lastSynced);
redis.set("last_sync:" + key, current);
}
});
}
6. 常见问题排查
6.1 误拦截分析
我们遇到过一个典型案例:某企业用户的所有员工突然无法登录。排查发现:
- 这些员工共用企业出口IP
- 登录接口设置了每分钟20次的限制
- 上班打卡时段集中登录触发限制
解决方案:
- 对企业IP设置白名单
- 或识别User-Agent中的企业标识
- 或改为按用户ID+IP复合限制
6.2 性能问题排查
当接口响应变慢时,按以下步骤排查:
- 使用Arthas监控计数器锁竞争:
bash复制watch com.example.counter.SlidingWindowCounter increment '{params,returnObj}' -x 3
- 检查时间片滑动逻辑是否过于频繁
- 验证Guava缓存是否达到最大容量
7. 生产环境配置建议
7.1 推荐参数配置
根据业务类型推荐不同配置:
| 接口类型 | 窗口大小 | 最大请求数 | 惩罚措施 |
|---|---|---|---|
| 登录接口 | 60秒 | 10次 | 图形验证码 |
| 短信接口 | 3600秒 | 5次/手机号 | 24小时锁定 |
| 支付接口 | 300秒 | 3次 | 需人工验证 |
7.2 监控指标配置
建议在Prometheus中监控以下指标:
yaml复制- name: api_request_count
type: Counter
help: Total API requests
labels: [api, status]
- name: api_limit_hits
type: Gauge
help: Number of requests blocked by limit
labels: [api, ip]
在Grafana中配置的告警规则:
- 同一IP在5分钟内触发超过3次限制
- 某个接口的拦截率突然超过30%
- 验证码展示次数环比增长200%
