1. 先搞清楚:你遇到的到底是不是CC攻击
很多人一看到服务器CPU飙到100%、网站打不开、带宽跑满,第一反应就是“完了,被CC了”。这个判断本身就可能把你带偏。CC攻击(Challenge Collapsar)本质上是应用层的资源消耗型攻击,它不像流量型攻击那样靠大带宽硬砸,而是模拟正常用户的行为,反复请求那些“计算成本高、数据库压力大、不容易缓存”的接口,用很小的流量把你的后端拖垮。所以判断的第一步不是看流量大小,而是看请求特征和后端资源消耗的对应关系。
我见过太多案例,运维同学一上来就加机器、上高防,结果钱花了不少,问题依旧。原因很简单:如果是慢查询或者代码死循环导致的CPU高,你加多少机器都是白搭。所以先做三件事:看访问日志里有没有大量来自同一批IP、同一批User-Agent、请求路径高度集中的记录;看数据库的慢查询日志和连接数是不是在攻击时段暴涨;看Nginx或应用层的access.log里,单个IP的请求频率是不是远超正常用户。这三条里中了两条以上,基本可以锁定是CC。
注意:不要只盯着QPS看。有些CC攻击的QPS并不高,比如每秒只打几十个请求,但每个请求都触发一次全表扫描或者复杂计算,照样能把数据库打挂。这种叫“低频高耗”型CC,最容易被误判成“网站变慢了”。
还有一个容易被忽略的点:CC攻击经常和爬虫、恶意采集混在一起。有些攻击者先用爬虫把你的动态接口摸一遍,找到最耗资源的那个,然后集中火力打。所以你在排查时,不妨把最近新增的、请求参数异常规律的访问也纳入观察范围。判断清楚性质,后面的处置才有方向,否则就是瞎忙活。
2. 应急止血:攻击正在进行时的处置顺序
攻击正在发生的时候,最忌讳的是“边想边改”。我建议你按一个固定的优先级顺序动手,先保住服务可用性,再谈精准封禁。顺序错了,可能你还在分析日志,业务已经彻底挂了。
2.1 第一优先级:把动态请求挡在应用层之外
如果你的站点前面有Nginx或者类似的接入层,最快的止血手段是在接入层做限速和限并发。这里的关键是区分“静态资源”和“动态接口”。静态资源(图片、CSS、JS)走缓存,基本不消耗后端,不用管;真正要限的是那些会打到应用服务器和数据库的路径。
一个可以直接抄的Nginx配置思路:
# 定义限速区域,按IP维度,每秒10个请求 limit_req_zone $binary_remote_addr zone=cc_limit:10m rate=10r/s; server { location /api/ { limit_req zone=cc_limit burst=20 nodelay; limit_conn addr 10; proxy_pass http://backend; } }rate=10r/s的意思是每个IP每秒最多10个请求,burst=20允许突发20个排队,nodelay表示超出的直接拒绝而不是排队等待。这个值不是拍脑袋定的,你要先看正常用户的请求频率分布——大部分用户浏览页面时,动态接口的请求间隔都在秒级以上,所以10r/s对正常用户完全够用,对攻击者则是致命的。
提示:限速阈值一定要结合你自己的业务来调。电商详情页和后台管理接口的合理频率完全不同,一刀切会误伤。建议先在测试环境用真实访问日志回放一遍,确认正常用户不会被拦。
2.2 第二优先级:临时封禁高频来源
限速是“减速带”,封禁是“直接关门”。当你从日志里提取出一批请求频率异常高的IP段时,可以用防火墙或者接入层的deny规则临时封掉。这里有个经验:不要一个个IP封,要按C段封。CC攻击的来源往往集中在少数几个C段里,逐个封IP既慢又容易漏。
# 统计访问量前20的IP awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20 # 统计访问量前10的C段 awk -F. '{print $1"."$2"."$3".0/24"}' access.log | sort | uniq -c | sort -rn | head -10拿到C段列表后,在防火墙层批量封禁。但要注意,封禁是有代价的——如果攻击者用了大量分散的代理IP,你封C段可能误伤正常用户。所以这一步适合“来源集中”的攻击,如果来源极其分散,就老老实实回到限速和后面的行为识别方案。
2.3 第三优先级:给数据库和应用加缓冲
如果攻击已经打到了数据库,光在接入层限速可能来不及。这时候要临时给应用层加一些“泄压阀”:把非核心接口的数据库查询临时改成读缓存,把一些统计类、列表类的接口临时降级返回静态数据,把连接池的最大连接数调低并设置合理的超时。这些操作的目的不是根治,而是让核心链路活下来。
我踩过的一个坑是:攻击时数据库连接池被打满,导致正常用户的请求全部超时,但数据库本身CPU并不高。后来查出来是应用层的连接没有及时释放,攻击请求把连接占着不放。所以应急时除了限流,一定要检查连接池的maxActive、maxWait和超时配置,必要时临时调小,让连接快速周转。
3. 为什么加了机器还是扛不住:CC攻击的资源账要算明白
这是很多人最困惑的地方:明明加了带宽、加了服务器,怎么还是挂?因为CC攻击消耗的不是你想象中的那种资源。理解这一点,你才能选对防御手段。
3.1 CC打的是“计算”,不是“带宽”
一个典型的CC请求可能是这样的:请求一个需要联合三张表、做一次排序、再调用一次外部接口的页面。这个请求的响应体可能只有几KB,带宽占用微乎其微,但它消耗的CPU时间和数据库IO是普通静态请求的几十倍甚至上百倍。攻击者用几百个并发就能让你的应用服务器CPU跑满,而你的带宽监控看起来一切正常。
这就解释了一个常见现象:带宽没满,但服务挂了。如果你只盯着带宽做防御,自然会觉得“加了带宽怎么没用”。正确的资源账应该是:估算单个高耗请求的CPU耗时,乘以攻击QPS,看是否超过你后端的总处理能力。比如单请求耗时50ms,攻击QPS是200,那就是每秒10秒的CPU工作量,一台单核机器直接满负荷。这个账算清楚,你就知道该往哪个方向使劲了。
3.2 缓存为什么救不了动态接口
有人会说,上CDN、上缓存不就行了?问题是CC攻击专门挑那些不能缓存的接口打。用户登录、订单查询、搜索、评论提交,这些接口的响应因人而异、因参数而异,缓存命中率极低。你就算上了CDN,这些请求还是会穿透到源站。所以缓存对CC的防御效果有限,真正有效的是在请求到达应用之前就把它识别出来并拦掉。
3.3 弹性扩容的陷阱
“被打了就自动扩容”听起来很美好,但CC攻击下自动扩容可能是个陷阱。因为攻击流量是持续的,你扩容到10台,攻击者把QPS翻一倍,你又得扩到20台,成本失控。更糟的是,如果扩容触发的是应用层实例,而瓶颈在数据库,那扩容反而会加剧数据库压力。所以弹性扩容只适合应对突发正常流量,面对持续CC,必须配合请求识别和拦截,否则就是往无底洞里扔钱。
4. 从“被动挨打”到“主动识别”:行为特征才是关键
限速和封IP都是被动防御,攻击者换个IP、降点频率就能绕过。真正有效的方案是基于行为特征做识别,让系统能区分“正常用户”和“机器请求”。这部分是整个防御体系里技术含量最高、也最值得投入的地方。
4.1 几个高性价比的识别维度
不需要一上来就搞复杂的机器学习模型,几个基础维度就能拦掉大部分低质量CC:
| 识别维度 | 正常用户特征 | CC攻击特征 | 落地方式 |
|---|---|---|---|
| 请求间隔 | 有思考停顿,间隔不均 | 间隔极短且均匀 | 接入层记录时间戳做方差分析 |
| 请求路径 | 有页面跳转逻辑 | 直接打接口,跳过静态资源 | 检查Referer和资源加载顺序 |
| User-Agent | 多样且真实 | 集中、伪造、版本异常 | 维护UA黑名单+格式校验 |
| Cookie/Session | 有完整会话 | 无会话或会话复用 | 校验会话有效性 |
| 参数规律 | 参数随机自然 | 参数递增或固定 | 参数熵值分析 |
其中请求间隔的方差是我认为性价比最高的指标。正常用户点一下等几秒再看下一个,间隔忽长忽短;脚本请求的间隔往往非常均匀,方差极小。你甚至不需要复杂的算法,在接入层记录每个IP最近N次请求的时间戳,算一下间隔的标准差,低于某个阈值就触发验证。
4.2 验证码的正确用法
验证码是区分人和机器最直接的手段,但用错了会严重伤害正常用户体验。我的经验是:不要全局弹验证码,要按风险分级触发。低风险请求直接放行,中风险请求弹一个轻量验证(比如点击验证),高风险请求才上复杂验证。触发条件可以组合:单IP频率超阈值、请求间隔方差过小、UA异常、无有效会话,命中两条以上才弹。
还有一个细节:验证码的下发和校验要在接入层完成,不要打到应用服务器。否则攻击者连验证码接口一起打,你等于给自己增加了一个高耗接口。接入层做验证,通过后再放行到后端,这样验证码本身也成了一道过滤网。
4.3 人机识别之后还要做什么
通过验证的请求不代表绝对安全,有些高级CC会雇佣真人打码或者用打码平台绕过验证。所以验证通过后,还要对通过验证的会话做持续观察:这个会话后续的请求频率、路径分布是否仍然异常。如果异常,降级处理或者二次验证。这套“验证+持续观察”的组合,能把防御成本压得很低,同时拦住绝大多数自动化攻击。
5. 长期建设:把防御做成常态而不是救火
每次被打了才手忙脚乱,说明防御体系还停留在“救火”阶段。真正省心的做法是把几件事做成常态化的机制,让攻击来的时候系统自己能扛住第一波。
5.1 日志和监控要能“看见”攻击
很多团队的问题是:攻击发生了,但日志里看不出来。要么是日志级别太低没记录关键字段,要么是日志量太大被轮转覆盖了。我的建议是:接入层日志至少保留IP、时间戳、请求路径、状态码、响应时间、User-Agent、Referer这几个字段,并且按小时做聚合统计。监控上设置几个告警:单IP请求频率突增、某接口平均响应时间突增、数据库连接数突增、5xx错误率突增。这几个告警组合起来,基本能在攻击初期就发现异常。
提示:日志聚合不要只做总量统计,要能按IP、按路径、按UA下钻。攻击的特征往往藏在细分维度里,总量看不出来,一下钻就原形毕露。
5.2 把高频接口的“成本”降下来
防御的另一个思路是让攻击“不划算”。具体做法是优化那些最容易被CC盯上的接口:能加缓存的加缓存,能异步的改异步,能拆分的拆分。比如一个复杂的统计接口,原来每次请求都实时算,改成定时预计算+缓存读取,单请求成本从几百毫秒降到几毫秒,攻击者打同样的QPS,对你的伤害就小了一个数量级。这属于“治本”的工作,平时做比攻击时做从容得多。
5.3 准备一套可复用的应急预案
应急的时候最怕临时想方案。建议提前写好一份处置清单,按“确认攻击类型→接入层限速→封禁高频来源→数据库泄压→通知相关方”的顺序列清楚每一步的具体操作命令和配置位置。攻击来的时候照着执行,比现场讨论快得多。这份清单还要定期演练,确保配置改动能快速生效、回滚也方便。
6. 几个我踩过的坑和反直觉的经验
最后分享几个实际操作中总结出来的点,都是文档里不太会写、但真遇到时很要命的东西。
第一个坑:限速配置写错位置。有次在Nginx里把limit_req写在了server块而不是location块,结果静态资源也被限速,正常用户加载页面时图片被拦,体验极差。限速一定要精确到需要保护的动态路径上,别图省事全局加。
第二个坑:封IP封到了自己的监控。批量封C段的时候没排除自家监控和合作方的IP段,结果攻击是挡住了,监控也瞎了,过了半天才发现。封禁前一定要把白名单过一遍,尤其是监控、CDN回源、第三方回调这些固定来源。
第三个反直觉的经验:有时候“慢”比“快”更安全。面对CC,有些团队会拼命优化响应速度,结果单请求成本降低后,同样的攻击QPS能打出更高的总量。真正该做的是在接入层就把可疑请求拦掉,而不是让它们进到后端再比谁快。拦截的位置越靠前,后端越安全。
第四个经验:别迷信单一手段。限速、封禁、验证码、行为识别,没有哪个是万能的。攻击者会绕过单一手段,但组合起来用,绕过成本就高得多。防御体系的价值不在于某一道墙有多高,而在于让攻击者每前进一步都要付出代价。我在实际配置时,通常会让限速做第一道粗筛,行为识别做第二道精筛,验证码做兜底,三层配合下来,大部分CC在第二层就被拦住了,真正打到后端的流量很小。
这套思路不是一次配好就一劳永逸的,攻击手法在变,你的阈值和规则也要跟着调。但只要把“看清攻击、快速止血、行为识别、常态建设”这条主线走通,下次再遇到服务器被CC,你至少不会慌。