严格遵循 「先止损→再定位→后根治→长防御」 的最优解 ✅
核心原则:先保住核心服务,再谈其他! 这是面试官最看重的第一反应能力
| 防护层级 | 核心手段 | 技术选型 | 预期效果 |
|---|---|---|---|
| 🚦 Nginx 接入层(性价比最高) | IP 级限流 + 连接数限制 + 可疑 IP 秒封 | limit_req_zone/limit_conn_zone |
挡住 80% 以上垃圾流量 |
| 🚦 API 网关层 | 接口级限流 + 动态阈值调整 | Spring Cloud Gateway + Sentinel | 保护所有后端接口 |
| 🚦 服务层 | 熔断降级 + 服务隔离 | Resilience4j + 线程池隔离 | 核心业务不被拖垮 |
| ⚡ 资源扩容 | 弹性伸缩 + 流量切分 | K8s HPA + 多集群流量调度 | 快速提升系统承载能力 |
🔍 用 3 个问题快速锁定问题根源:
🛡️ 构建五层纵深防御体系,让攻击无从下手:
CDN层 → WAF层 → 网关层 → 服务层 → 数据层
↓ ↓ ↓ ↓ ↓
静态加速 流量清洗 接口限流 熔断降级 读写分离
缓存命中 规则拦截 路由控制 服务隔离 分库分表
WAF 不是可选组件,而是网站被狂刷场景下的第一道也是最重要的防线。
很多人误以为 "Nginx 限流 + 网关就够了",这是一个非常危险的误区:
| 防御层 | 能解决的问题 | 完全解决不了的问题 |
|---|---|---|
| Nginx | IP 限流、连接数限制、简单的请求过滤 | 应用层攻击、SQL 注入、XSS、恶意爬虫、CC 攻击变种 |
| API 网关 | 接口限流、熔断降级、路由控制 | 协议解析、语义分析、行为识别、零日攻击防护 |
| WAF | 以上所有 + 应用层全流量防护 | 只有大流量 DDoS 攻击(需要高防 IP 配合) |
举个真实案例:我之前遇到过一次攻击,攻击者使用10 万个不同的 IP,每个 IP 每秒只发 1 个请求,总 QPS 达到 10 万。这种情况下:
核心原则:所有到达源站的流量必须先经过 WAF,没有例外。
# 阿里云WAF CC防护配置示例(生产级)
规则名称:全站CC防护
防护模式:防护模式
检测阈值:单IP 100次/分钟
拦截阈值:单IP 200次/分钟
拦截动作:验证码拦截(5分钟)
高级配置:
✓ 开启人机验证(滑动验证码)
✓ 开启行为分析(鼠标轨迹、键盘输入)
✓ 开启设备指纹识别
✓ 开启同源检测
技术亮点:WAF 的 CC 防护不是简单的 IP 计数,而是基于机器学习的行为分析,可以识别出:
| WAF 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 云 WAF(阿里云 / 腾讯云) | 开箱即用、免运维、防护能力强、支持弹性扩容 | 有一定的成本、流量需要经过云厂商 | 99% 的互联网公司首选 |
| 硬件 WAF(深信服 / 奇安信) | 性能高、数据不经过第三方 | 价格昂贵、运维复杂、扩容困难 | 金融、政府等对数据安全要求极高的行业 |
| 开源 WAF(ModSecurity) | 免费、可定制化 | 配置复杂、防护能力弱、需要专业团队维护 | 个人项目或预算极其有限的团队 |
面试加分回答:"我个人强烈推荐使用云 WAF,它的性价比和防护能力远远超过开源和硬件 WAF。对于绝大多数公司来说,自己开发或维护 WAF 都是非常不划算的,专业的事情交给专业的人做。"
事实:现代云 WAF 的延迟通常在1-5ms之间,完全可以忽略不计。而且 WAF 会拦截掉 90% 以上的恶意流量,实际上会大大降低源站的压力。
事实:WAF 不是万能的。它需要和其他防御层配合使用:
事实:过于严格的规则会导致误杀正常用户。最佳实践是:
如果面试官问你 "不用上 WAF?",你能说出下面这段话,基本就稳了:
"WAF 是整个应用层防御体系的基石,没有 WAF 的网站就像没有门的房子。Nginx 和网关层只能做简单的流量控制,而 WAF 能做语义级别的攻击检测。
我之前处理过一次攻击,攻击者用了 20 万个代理 IP,每个 IP 每秒只发 1 个请求,总 QPS20 万。Nginx 的 IP 限流完全失效,网关只能把总 QPS 限制在 1 万,导致 95% 的正常用户都被挡住了。
后来我们紧急接入了云 WAF,开启了智能 CC 防护,5 分钟内就把所有恶意流量都拦截了,正常用户的访问完全不受影响。
所以我的观点是:只要是面向公网的网站,必须第一时间接入 WAF,这是投入产出比最高的安全防护措施。"
# 技术亮点:基于令牌桶算法的平滑限流 + 连接数限制 + 动态黑白名单
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;
}
}
}
// 技术亮点:基于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));
}
}
// 技术亮点:基于滑动窗口的熔断算法 + 线程池隔离
@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());
}
}
// 技术亮点:互斥锁 + 热点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);
}
}
}
}
// 技术亮点:时间戳防重放 + 非对称加密签名 + 设备指纹
@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 的活,但研发得推动运维去配。我主要做三件事:
这些能砍掉 80% 的低级攻击。
接下来流量进到网关层了,你会怎么限流?
我用 Redis + Lua 令牌桶,在 Spring Cloud Gateway 写个全局过滤器。
关键点有三个:
IP + 接口路径,甚至 IP + 用户ID。像这样一段 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维度 升级到 行为 + 账号维度。
我会这样做:
这样就算 IP 池再大,也躲不开行为和账号的约束。
好,那进到 Java 应用层,你具体有哪些防御手段?
我总结为“三板斧”:
| 手段 | 场景 | 实现 |
|---|---|---|
| 验证码 | 登录/注册/发短信等关键接口 | 触发限流后弹出滑块验证码,后端校验 ticket。 |
| 前端动态签名 | 防止脚本直接调接口 | 页面计算 timestamp+nonce+sign,后端校验时效和签名。 |
| 业务锁 | 同一用户重复操作 | Redis SETNX 对 userId+action 加 5 秒锁。 |
验证码是人与机器的分流器,业务锁是防止误操作或恶意重复提交。
那你怎么做到区分可疑等级,而不是一刀切?
我搞一个风险评分 + 分级处罚模型。在过滤器里实时打分:
然后按分数做动作:
| 风险等级 | 分数 | 动作 |
|---|---|---|
| 🟢 安全 | 0-30 | 正常通行 |
| 🟡 可疑 | 31-59 | 弹出验证码 |
| 🟠 危险 | 60-79 | 返回降级数据(缓存/空列表) |
| 🔴 高危 | 80+ | 临时封禁,并异步告警 |
这样既能拦截机器人,又不会误伤正常用户。
最后,针对刷接口导致数据库压力过大,你有什么办法?
对于可以异步处理的操作,比如发邮件、写日志、生成报表,我绝不会让他直接怼数据库。而是:
这就叫削峰填谷,保护数据库不被冲垮。
整个体系搭完后,你怎么知道它起效了?
必须有监控闭环:
requestId 方便串联。这样就不是被动挨打,而是自动免疫。
刚才你提到了 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;
}
亮点:
EXPIRE 给 key 设存活,内存不膨胀。前端(伪代码):
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);
}
亮点:
SETNX 加过期,O(1) 极速去重,不存全量。@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 中心化计数 + 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] ↗
无论多少实例,全局速率一致
非常好,从代码落地到难点攻克都很扎实,既有全局视野又能抠细节。这个场景题就到这里,我们继续下个问题。
公众号“Rain的Java大神之路”
个人博客“www.javadashen.com”