PC版网站被狂刷怎么处理
PC版网站被狂刷怎么处理
面试官您好,针对 PC 网站被狂刷这个高频场景设计题,我会严格遵循 「先止损→再定位→后根治→长防御」 的工业界标准流程来处理,这是我在多次大促和攻击事件中验证过的最优解 ✅
第一阶段:紧急止损(黄金 10 分钟)
核心原则:先保住核心服务,再谈其他! 这是面试官最看重的第一反应能力
| 防护层级 | 核心手段 | 技术选型 | 预期效果 |
|---|---|---|---|
| 🚦 Nginx 接入层(性价比最高) | IP 级限流 + 连接数限制 + 可疑 IP 秒封 | limit_req_zone/limit_conn_zone | 挡住 80% 以上垃圾流量 |
| 🚦 API 网关层 | 接口级限流 + 动态阈值调整 | Spring Cloud Gateway + Sentinel | 保护所有后端接口 |
| 🚦 服务层 | 熔断降级 + 服务隔离 | Resilience4j + 线程池隔离 | 核心业务不被拖垮 |
| ⚡ 资源扩容 | 弹性伸缩 + 流量切分 | K8s HPA + 多集群流量调度 | 快速提升系统承载能力 |
关键操作:
- 非核心接口(评论、收藏、推荐)直接熔断,返回静态降级页
- 静态资源(JS/CSS/ 图片)全量切 CDN,源站只处理动态请求
- 关闭数据库慢查询和实时统计功能,优先保证写操作
第二阶段:快速定位(止损后立即执行)
🔍 用 3 个问题快速锁定问题根源:
- 流量从哪来? 查 Nginx 日志 Top10 IP、User-Agent、请求接口
- 攻击类型是什么? CC 攻击?DDoS?爬虫?薅羊毛?
- 系统瓶颈在哪? CPU?内存?数据库连接?Redis 缓存击穿?
第三阶段:针对性根治(精准打击)
第四阶段:长期防御体系(一劳永逸)
🛡️ 构建五层纵深防御体系,让攻击无从下手:
CDN层 → WAF层 → 网关层 → 服务层 → 数据层
↓ ↓ ↓ ↓ ↓
静态加速 流量清洗 接口限流 熔断降级 读写分离
缓存命中 规则拦截 路由控制 服务隔离 分库分表核心建设点:
- 📊 全链路监控告警:QPS、响应时间、错误率、数据库连接数等指标秒级监控,设置多级告警阈值
- 🧪 定期全链路压测:提前发现系统瓶颈,验证限流降级策略有效性
- 📝 应急预案演练:针对不同攻击场景制定 SOP,每季度至少演练一次
- 🔒 安全加固:接口加密、参数校验、防止 SQL 注入 / XSS 攻击
🔴 为什么 Nginx / 网关层完全不能替代 WAF?
WAF 不是可选组件,而是网站被狂刷场景下的第一道也是最重要的防线。
很多人误以为 "Nginx 限流 + 网关就够了",这是一个非常危险的误区:
| 防御层 | 能解决的问题 | 完全解决不了的问题 |
|---|---|---|
| Nginx | IP 限流、连接数限制、简单的请求过滤 | 应用层攻击、SQL 注入、XSS、恶意爬虫、CC 攻击变种 |
| API 网关 | 接口限流、熔断降级、路由控制 | 协议解析、语义分析、行为识别、零日攻击防护 |
| WAF | 以上所有 + 应用层全流量防护 | 只有大流量 DDoS 攻击(需要高防 IP 配合) |
举个真实案例:我之前遇到过一次攻击,攻击者使用10 万个不同的 IP,每个 IP 每秒只发 1 个请求,总 QPS 达到 10 万。这种情况下:
- Nginx 的 IP 限流完全失效(每个 IP 都在限流阈值内)
- 网关的接口限流只能限制总 QPS,但会把正常用户也挡住
- 只有 WAF 能通过行为分析识别出这些请求都是机器发出的,然后一键拦截
🛡️ WAF 在五层纵深防御体系中的准确位置
核心原则:所有到达源站的流量必须先经过 WAF,没有例外。
🎯 WAF 的核心功能(网站被狂刷时必开)
1. 智能 CC 攻击防护(最核心)
# 阿里云WAF CC防护配置示例(生产级)
规则名称:全站CC防护
防护模式:防护模式
检测阈值:单IP 100次/分钟
拦截阈值:单IP 200次/分钟
拦截动作:验证码拦截(5分钟)
高级配置:
✓ 开启人机验证(滑动验证码)
✓ 开启行为分析(鼠标轨迹、键盘输入)
✓ 开启设备指纹识别
✓ 开启同源检测技术亮点:WAF 的 CC 防护不是简单的 IP 计数,而是基于机器学习的行为分析,可以识别出:
- 固定间隔的机器请求
- 没有 Referer 的请求
- User-Agent 异常的请求
- Cookie 不完整的请求
- 访问路径异常的请求
2. 恶意爬虫防护
- 自动识别并拦截搜索引擎爬虫以外的所有爬虫
- 支持自定义爬虫规则(如拦截特定 User-Agent、特定 IP 段)
- 支持爬虫限流(允许慢速爬取,禁止狂刷)
3. 虚拟补丁
- 不需要修改业务代码,就能防护已知的漏洞(如 Log4j、Spring Boot 漏洞)
- 这在紧急情况下非常重要,可以在几分钟内完成漏洞修复
4. 精准访问控制
- 支持基于 IP、地区、User-Agent、Cookie、请求头等多种维度的精准控制
- 例如:可以一键拦截所有来自境外的请求
📊 不同类型 WAF 的选型对比
| WAF 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 云 WAF(阿里云 / 腾讯云) | 开箱即用、免运维、防护能力强、支持弹性扩容 | 有一定的成本、流量需要经过云厂商 | 99% 的互联网公司首选 |
| 硬件 WAF(深信服 / 奇安信) | 性能高、数据不经过第三方 | 价格昂贵、运维复杂、扩容困难 | 金融、政府等对数据安全要求极高的行业 |
| 开源 WAF(ModSecurity) | 免费、可定制化 | 配置复杂、防护能力弱、需要专业团队维护 | 个人项目或预算极其有限的团队 |
面试加分回答:"我个人强烈推荐使用云 WAF,它的性价比和防护能力远远超过开源和硬件 WAF。对于绝大多数公司来说,自己开发或维护 WAF 都是非常不划算的,专业的事情交给专业的人做。"
⚠️ WAF 的常见误区和最佳实践
误区 1:WAF 会影响性能
事实:现代云 WAF 的延迟通常在1-5ms之间,完全可以忽略不计。而且 WAF 会拦截掉 90% 以上的恶意流量,实际上会大大降低源站的压力。
误区 2:开了 WAF 就万事大吉了
事实:WAF 不是万能的。它需要和其他防御层配合使用:
- WAF 拦截 90% 的恶意流量
- Nginx 拦截剩下的 9%
- 网关和服务层拦截最后 1%
误区 3:WAF 规则越严格越好
事实:过于严格的规则会导致误杀正常用户。最佳实践是:
- 先开启 "观察模式" 运行 1-2 周
- 分析误杀日志,调整规则
- 再切换到 "防护模式"
✨ 面试终极加分项
如果面试官问你 "不用上 WAF?",你能说出下面这段话,基本就稳了:
"WAF 是整个应用层防御体系的基石,没有 WAF 的网站就像没有门的房子。Nginx 和网关层只能做简单的流量控制,而 WAF 能做语义级别的攻击检测。
我之前处理过一次攻击,攻击者用了 20 万个代理 IP,每个 IP 每秒只发 1 个请求,总 QPS20 万。Nginx 的 IP 限流完全失效,网关只能把总 QPS 限制在 1 万,导致 95% 的正常用户都被挡住了。
后来我们紧急接入了云 WAF,开启了智能 CC 防护,5 分钟内就把所有恶意流量都拦截了,正常用户的访问完全不受影响。
所以我的观点是:只要是面向公网的网站,必须第一时间接入 WAF,这是投入产出比最高的安全防护措施。"
🧩 核心代码实现(生产级・带技术亮点)
1. Nginx 接入层限流(性价比最高,挡住 80% 流量)
# 技术亮点:基于令牌桶算法的平滑限流 + 连接数限制 + 动态黑白名单
http {
# 定义限流规则:10r/s 平滑限流,突发允许20个请求,超过直接返回503
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;
# 单个IP最大连接数限制
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
listen 80;
server_name example.com;
# 全局限流配置
limit_req zone=req_limit burst=20 nodelay;
limit_conn conn_limit 50;
# 静态资源直接走CDN,源站不处理
location ~* \.(js|css|png|jpg|gif|ico)$ {
expires 30d;
add_header Cache-Control "public, immutable";
# 静态资源不限流
limit_req off;
}
# 核心接口单独限流(更严格)
location /api/login {
limit_req zone=req_limit burst=5 nodelay;
proxy_pass http://backend;
}
# 非核心接口:异常时直接返回静态降级页
location /api/comment {
error_page 503 /static/503_comment.html;
proxy_pass http://backend;
}
}
}2. Spring Cloud Gateway + Sentinel 动态接口限流
// 技术亮点:基于Nacos的动态限流规则配置 + 热点参数限流
@Configuration
public class SentinelConfig {
// 初始化Sentinel规则
@PostConstruct
public void initRules() {
// 1. 全局流量控制规则
FlowRule flowRule=new FlowRule();
flowRule.setResource("all_api");
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRule.setCount(10000); // 集群总QPS限制
flowRule.setStrategy(RuleConstant.STRATEGY_DIRECT);
flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
// 2. 热点参数限流(防止单个IP/用户狂刷)
ParamFlowRule paramRule=new ParamFlowRule();
paramRule.setResource("api_login");
paramRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
paramRule.setCount(5); // 单个参数值每秒最多5次
paramRule.setParamIdx(0); // 第一个参数:IP地址
FlowRuleManager.loadRules(Collections.singletonList(flowRule));
ParamFlowRuleManager.loadRules(Collections.singletonList(paramRule));
}
}3. Resilience4j 熔断降级(防止服务雪崩)
// 技术亮点:基于滑动窗口的熔断算法 + 线程池隔离
@RestController
@RequestMapping("/api")
public class CommentController {
// 评论服务:独立线程池隔离,不影响核心服务
@Bulkhead(name="comment-service", fallbackMethod="commentFallback")
@CircuitBreaker(name="comment-service", fallbackMethod="commentFallback")
@GetMapping("/comment/list")
public ResponseEntity<List<Comment>> listComments(Long articleId) {
List<Comment> comments=commentService.listByArticleId(articleId);
return ResponseEntity.ok(comments);
}
// 降级方法:返回静态默认数据
public ResponseEntity<List<Comment>> commentFallback(Long articleId, Exception e) {
log.warn("评论服务降级,articleId: {}", articleId, e);
return ResponseEntity.ok(Collections.emptyList());
}
}4. Redis 防缓存击穿(热点 key 问题终极解决方案)
// 技术亮点:互斥锁 + 热点key永不过期 + 异步更新
@Component
public class RedisCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
// 获取热点数据(防击穿)
public String getHotData(String key, Function<String, String> loader) {
String value=redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 缓存不存在,尝试获取分布式锁
String lockKey="lock:"+key;
Boolean locked=redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 只有一个线程去数据库加载数据
value=loader.apply(key);
if (value != null) {
// 热点key永不过期,后台异步更新
redisTemplate.opsForValue().set(key, value);
// 异步更新任务
asyncUpdateCache(key, loader);
}
return value;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 其他线程等待100ms后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getHotData(key, loader);
}
}
// 异步更新缓存
@Async
public void asyncUpdateCache(String key, Function<String, String> loader) {
while (true) {
try {
Thread.sleep(60000); // 每分钟更新一次
String value=loader.apply(key);
if (value != null) {
redisTemplate.opsForValue().set(key, value);
}
} catch (Exception e) {
log.error("异步更新缓存失败,key: {}", key, e);
}
}
}
}5. 接口签名验证(防止接口被恶意调用)
// 技术亮点:时间戳防重放 + 非对称加密签名 + 设备指纹
@Component
public class ApiSignInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求参数
String timestamp=request.getHeader("X-Timestamp");
String sign=request.getHeader("X-Sign");
String deviceId=request.getHeader("X-Device-Id");
// 2. 时间戳校验:防止重放攻击(有效期5分钟)
long now=System.currentTimeMillis();
if (Math.abs(now-Long.parseLong(timestamp))>300000) {
response.setStatus(401);
return false;
}
// 3. 签名校验
String signStr=deviceId+timestamp+request.getRequestURI();
String serverSign=DigestUtils.md5DigestAsHex((signStr+"secret_key").getBytes());
if (!serverSign.equals(sign)) {
response.setStatus(401);
return false;
}
return true;
}
}🎯 技术难点与解决方案对照表
| 技术难点 | 问题描述 | 解决方案 | 技术亮点 |
|---|---|---|---|
| 服务雪崩效应 | 一个非核心服务被打垮,导致整个调用链崩溃,核心服务也不可用 | 线程池隔离 + 熔断降级 | 每个服务使用独立线程池,异常时自动熔断并返回降级数据 |
| IP 封禁误杀问题 | 简单的 IP 封禁会误杀共享 IP 的正常用户(如公司内网、校园网) | 基于用户行为的风控 + 分级限流 | 不直接封 IP,而是对异常行为的用户进行限流;同一 IP 下不同用户独立计数 |
| 缓存击穿 / 雪崩 / 穿透 | 热点 key 过期导致数据库瞬间被打垮;大量 key 同时过期导致雪崩;恶意请求不存在的 key 导致穿透 | 击穿:互斥锁 + 热点 key 永不过期 雪崩:过期时间随机化 穿透:布隆过滤器 + 空值缓存 | 布隆过滤器提前拦截不存在的 key;空值缓存设置短过期时间 |
| 分布式限流一致性 | 单机限流无法控制集群总流量;分布式限流存在性能瓶颈 | Sentinel 集群限流 + 令牌桶算法 | 令牌桶算法实现平滑限流;集群限流使用 Redis 作为令牌中心 |
| 人机验证被绕过 | 简单的验证码容易被 AI 识别;滑块验证容易被脚本模拟 | 行为式验证码 + 设备指纹 + 风控评分 | 分析用户鼠标轨迹、键盘输入速度等行为特征;结合设备指纹识别恶意设备 |
| DDoS 攻击防护 | 大流量 DDoS 攻击直接打满带宽,源站无法响应 | 高防 IP + Anycast 流量清洗 + 隐藏源站 IP | 流量先经过高防机房清洗,只把正常流量转发到源站;源站 IP 不对外暴露 |
| 数据库压力过大 | 大量写请求导致数据库 CPU/IO 飙升,响应变慢 | 读写分离 + 分库分表 + 异步写入 | 读请求走从库;大表分库分表;非实时写请求先写入 MQ,异步消费到数据库 |
真实面试模拟
真实面试模拟
面试官 😊:
欢迎,今天咱们聊个场景题。假设咱们公司有个 PC 版网站,最近被黑产用脚本狂刷接口——有爬数据的、扫号的、恶意提交的。你作为 Java 研发负责人,会怎么设计一套防护方案?
候选人 🧐:
我会搞一个纵深防御,从外到内层层过滤,不能指望单点解决。大概是这样:
用户 → CDN/WAF 边缘 → Nginx/网关 → Java 应用 → 数据库
▲ ▲ ▲
① 脏流量丢弃 ② 令牌桶限流 ③ 业务风控+异步削峰先把脏东西挡在门外,到了 Java 层只处理相对干净的流量。
面试官 👍:
思路不错。那先从边缘层说起,你会做什么?
候选人 💻:
这一层不是 Java 的活,但研发得推动运维去配。我主要做三件事:
- CDN + WAF 速率限制:比如阿里云WAF,设置单 IP 每秒超过 20 次就弹出 JS 挑战,直接过滤掉脚本。
- IP 信誉库:接入威胁情报,把已知的代理、IDC 机房 IP 直接拉黑。
- 地域封禁:如果业务仅服务国内,就把海外流量全部拒绝。
这些能砍掉 80% 的低级攻击。
面试官 🚪:
接下来流量进到网关层了,你会怎么限流?
候选人:
我用 Redis + Lua 令牌桶,在 Spring Cloud Gateway 写个全局过滤器。
关键点有三个:
- 粒度要细:不能只限 IP,得限
IP + 接口路径,甚至IP + 用户ID。 - 允许突发:令牌桶比漏桶友好,能应对正常流量毛刺。
- 原子性:用 Lua 脚本保证判断和扣减在 Redis 里一次完成。
像这样一段 Lua:
-- 每次请求扣1个令牌,不够就返回0限流
local tokens = redis.call('HGET', KEYS[1], 'tokens')
tokens = tonumber(tokens or capacity)
if tokens >= 1 then
redis.call('HSET', KEYS[1], 'tokens', tokens - 1)
return 1
else
return 0
end超限就回 429,并且同一个 IP 连续超限三次,我直接把它扔进临时黑名单,冷置 5 分钟。
面试官 🤔:
很好。如果攻击者搞到了成千上万的家宽 IP,你的 IP 限流不就穿了?
候选人 🛡️:
确实,这时候就得从 IP维度 升级到 行为 + 账号维度。
我会这样做:
- 设备指纹:前端采集 Canvas、WebGL 等生成唯一设备 ID,对这个 ID 限流。他换 IP 也没用,除非连设备都换了。
- 业务硬限制:比如发短信接口,同一个手机号 1 分钟只能发 1 次,一天最多 5 次。这是铁律,换 IP 也没法绕过。
- 节奏分析:真人点击间隔是波动的,脚本是机械的。我可以在后台统计请求间隔的方差,方差极小就判定为脚本,直接踢挑战。
这样就算 IP 池再大,也躲不开行为和账号的约束。
面试官 🧱:
好,那进到 Java 应用层,你具体有哪些防御手段?
候选人 💪:
我总结为“三板斧”:
| 手段 | 场景 | 实现 |
|---|---|---|
| 验证码 | 登录/注册/发短信等关键接口 | 触发限流后弹出滑块验证码,后端校验 ticket。 |
| 前端动态签名 | 防止脚本直接调接口 | 页面计算 timestamp+nonce+sign,后端校验时效和签名。 |
| 业务锁 | 同一用户重复操作 | Redis SETNX 对 userId+action 加 5 秒锁。 |
验证码是人与机器的分流器,业务锁是防止误操作或恶意重复提交。
面试官 🚨:
那你怎么做到区分可疑等级,而不是一刀切?
候选人 ⚡:
我搞一个风险评分 + 分级处罚模型。在过滤器里实时打分:
- 同一 IP 频繁切换 User-Agent → +20 分
- 请求间隔高度一致(机器人特征)→ +15 分
- 总在凌晨搞事情 → +10 分
然后按分数做动作:
| 风险等级 | 分数 | 动作 |
|---|---|---|
| 🟢 安全 | 0-30 | 正常通行 |
| 🟡 可疑 | 31-59 | 弹出验证码 |
| 🟠 危险 | 60-79 | 返回降级数据(缓存/空列表) |
| 🔴 高危 | 80+ | 临时封禁,并异步告警 |
这样既能拦截机器人,又不会误伤正常用户。
面试官 📈:
最后,针对刷接口导致数据库压力过大,你有什么办法?
候选人 📬:
对于可以异步处理的操作,比如发邮件、写日志、生成报表,我绝不会让他直接怼数据库。而是:
- 扔消息队列(RocketMQ / Kafka),请求先入队。
- 后端限速消费,比如每秒处理 50 条。
- 如果队列积压超过阈值,前置层直接拒绝新请求,提示“系统繁忙”。
这就叫削峰填谷,保护数据库不被冲垮。
面试官 👏:
整个体系搭完后,你怎么知道它起效了?
候选人 📊:
必须有监控闭环:
- 埋点日志:所有拦截、限流都打日志,带
requestId方便串联。 - 监控大盘:Prometheus + Grafana 显示 QPS、限流触发率、封禁 IP 数。
- 自动联动:当某接口限流触发率超过 60%,自动告警,并通知运维调整 WAF 策略,把攻击 IP 段黑洞路由。
这样就不是被动挨打,而是自动免疫。
面试官 💻:
刚才你提到了 Redis Lua 令牌桶、前端动态签名、风险评分拦截器,这些概念不错。能不能挑一段你觉得最有技术亮点的代码,现场手写一下核心逻辑?不用完整,关键部分就行。
候选人 ⌨️:
没问题,我写三段,分别对应网关、应用、风控三层。
1. Redis Lua 令牌桶(网关层限流)
// Java侧调用
public boolean tryAcquire(String key, double rate, int capacity) {
String script =
"local tokens = redis.call('HGET', KEYS[1], 'tokens') " +
"tokens = tonumber(tokens or ARGV[2]) " + // 第一次初始化
"if tokens >= tonumber(ARGV[3]) then " +
" redis.call('HSET', KEYS[1], 'tokens', tokens - tonumber(ARGV[3])) " +
" redis.call('EXPIRE', KEYS[1], ARGV[4]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
List<String> keys = Collections.singletonList("rate:" + key);
List<String> args = Arrays.asList(
String.valueOf(rate), // ARGV[1]: 速率(令牌/秒)
String.valueOf(capacity), // ARGV[2]: 桶容量
"1", // ARGV[3]: 请求消耗令牌数
"10" // ARGV[4]: key过期时间(秒)
);
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class), keys, args.toArray()
);
return result == 1;
}亮点:
- 用 Lua 把“判断+扣减”打成原子操作,避免并发超发。
- 支持动态速率、动态容量,不同接口从配置中心拉参数。
- 通过
EXPIRE给 key 设存活,内存不膨胀。
2. 前端动态签名校验(应用层防脚本直接调接口)
前端(伪代码):
const timestamp = Date.now();
const nonce = generateUUID();
const params = { userId: 123, action: 'submit' };
const sign = md5(timestamp + nonce + JSON.stringify(params) + SECRET);Java 后端校验:
public boolean checkSign(HttpServletRequest request) {
String timestamp = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
String sign = request.getHeader("X-Sign");
String body = ...; // 读取请求体
// 1. 时效性检查(60秒过期)
if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) > 60000) {
return false;
}
// 2. Nonce去重(Redis Set判重)
String nonceKey = "nonce:" + nonce;
if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", 60, TimeUnit.SECONDS))) {
return false; // 重放攻击
}
// 3. 签名比对
String expected = DigestUtils.md5Hex(timestamp + nonce + body + SECRET);
return expected.equals(sign);
}亮点:
- 时间戳防“旧请求复用”,Nonce 防“单次重放”,签名防“参数篡改”。
- Nonce 用 Redis
SETNX加过期,O(1) 极速去重,不存全量。
3. 风险评分拦截器(应用层隐形风控)
@Component
public class RiskInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, Object> redis;
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String ip = getClientIp(request);
String deviceId = request.getHeader("X-Device-Id");
String userAgent = request.getHeader("User-Agent");
int score = 0;
// 规则1:同一IP频繁换UA(疑似代理池)
String lastUA = (String) redis.opsForValue().get("ua:" + ip);
if (lastUA != null && !lastUA.equals(userAgent)) {
score += 20;
}
redis.opsForValue().set("ua:" + ip, userAgent, 10, TimeUnit.MINUTES);
// 规则2:设备指纹异常(短时间内多个IP)
if (deviceId != null) {
Long ipCount = redis.opsForSet().size("device_ips:" + deviceId);
if (ipCount > 5) score += 15;
redis.opsForSet().add("device_ips:" + deviceId, ip);
redis.expire("device_ips:" + deviceId, 10, TimeUnit.MINUTES);
}
// 根据分数做动作(伪代码)
if (score > 30) {
// 弹出验证码
return challenge(request, response);
} else if (score > 60) {
// 临时封禁
return ban(request, response);
}
return true;
}
}亮点:
- 利用 Redis 存储短期行为特征,内存计算,几乎无延迟。
- 多维度叠加评分,越界即拦截,形成自愈式防御。
面试官 🔥:
代码功底不错。那我们再深挖一下,这个防刷场景会有哪些技术难点?你怎么解决的?
候选人 🧗:
我总结五大难点和对应解法:
| 难点 | 为什么难 | 解决方案 |
|---|---|---|
| 🔴 分布式下限流一致性 | 多实例网关各自限流,会出现总量超发 | ✅ 使用 Redis 中心化计数 + Lua 原子脚本。所有实例共用一个令牌桶,保证全局精准。 |
| 🟠 IP 代理池绕过 | 攻击者拥有海量家庭IP,IP限流失效 | ✅ 升级到 设备指纹 + 行为节奏分析。结合前端生成设备ID,后台统计请求间隔方差,低方差判定为脚本。 |
| 🟡 误伤正常用户 | 严格的规则可能拦截真实用户的突发操作(如双11抢购) | ✅ 采用 柔性处罚梯度(验证码→降级→封禁)。使用令牌桶允许突发。引入白名单(VIP用户、内部IP)避免误杀。 |
| 🟢 实时风控的性能开销 | 每次请求都要查 Redis、算分数,可能拖慢响应 | ✅ 用 管道化 + 本地缓存 优化。例如 IP 风险分本地缓存 1 秒,减少 Redis 交互。风险计算规则用轻量数据结构(Set/Hash)。 |
| 🔵 前端签名被逆向 | JS 代码里的加密逻辑和 SECRET 有暴露风险 | ✅ 动态混淆 + 短期有效。SECRET 定期刷新(如20分钟),由后端注入页面。JS 本身经混淆器处理,增加破解成本。配合设备指纹,即使签名被解,仍需过行为关。 |
特别是“分布式限流一致性”这个点,我画个对比图:
❌ 单机限流:各自为政
[实例A] → 计数器A (限10/s) 总允许量 = A+B = 20/s
[实例B] → 计数器B (限10/s) 实际可能超过预期
✅ 中心化限流
[实例A] ↘
→ Redis 令牌桶 (总容量10/s)
[实例B] ↗
无论多少实例,全局速率一致面试官 😄:
非常好,从代码落地到难点攻克都很扎实,既有全局视野又能抠细节。这个场景题就到这里,我们继续下个问题。
