遭遇CC攻击时,最直观的感受就是服务器资源被大量看似正常的请求耗尽。这些请求往往直接命中动态页面、搜索接口或数据库查询等消耗资源较大的URL。攻击者通过代理或肉鸡发起海量请求,导致正常用户无法访问。此时直接封IP效果甚微,因为攻击源分布太广。真正有效的第一道防线,是在流量抵达服务器核心处理逻辑之前,插入一层或多层验证步骤,用极小的计算成本来甄别和拦截自动化攻击流量,从而把宝贵的后端资源留给真实用户。
从HTTP层面强制浏览器执行JavaScript跳转很多CC攻击工具是脚本化运行的,它们通常只解析HTTP响应头中的状态码和Cookie,并不会完整执行页面中的JavaScript代码。利用这个特性,可以在用户首次访问时返回一个特殊的中间页面,这个页面不包含任何业务内容,只包含一段用于验证的JavaScript代码。代码执行后会生成一个带有特定令牌的跳转链接,或自动提交一个隐藏表单,将令牌传回服务器。服务器验证令牌合法后,才将用户重定向到真正的目标页面,并下发一个会话Cookie作为后续请求的凭证。
实现上,令牌的生成需要满足一次性、时效性和不可伪造性。服务器端可以使用HMAC算法,将当前时间戳、客户端IP和随机盐值组合后计算出一个哈希值,作为令牌嵌入到JavaScript中。当客户端回传令牌时,服务器重新计算并比对,同时检查时间戳是否在允许的窗口期内,比如30秒。超过时间窗口的令牌直接作废,要求重新验证。这样即使攻击者抓取到了令牌,也无法在过期后重复使用。
具体代码逻辑可以参考以下示例,这是Nginx配合Lua模块实现的一个简易验证中间页:
-- 生成令牌
local secret = "your-secret-salt"
local client_ip = ngx.var.remote_addr
local now = ngx.time()
local token = ngx.encode_base64(ngx.hmac_sha1(secret, client_ip .. now))
local redirect_url = ngx.var.scheme .. "://" .. ngx.var.host .. "/verify?token=" .. token .. "&t=" .. now
-- 输出验证页面
ngx.header.content_type = "text/html"
ngx.say([[
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>安全验证</title></head>
<body>
<script>
(function(){
var u = ']] .. redirect_url .. [[';
window.location.href = u;
})();
</script>
</body>
</html>
]])
在/verify路径上,服务器需要校验token的有效性,校验通过后设置一个HttpOnly且Secure的Session Cookie,然后将用户302重定向到原始请求的URL。后续请求中,只要检测到这个有效Cookie,就可以直接放行,不再走JavaScript验证流程。Cookie的有效期可以设置为几小时,避免正常用户频繁验证。这种方式的优势在于零用户交互,真实浏览器完全无感,而绝大多数脚本工具会卡在这一步。
引入交互式验证码作为升级策略当攻击流量巨大,或者攻击者已经升级到使用无头浏览器模拟JavaScript执行时,单纯的JS跳转验证可能会被绕过。此时需要增加交互式验证,也就是我们常说的人机验证。交互式验证的核心不是输入几个扭曲字符那么简单,而是通过收集用户在验证框中的行为数据来判断是否为真人操作。鼠标的移动轨迹、点击的间隔时间、滑动滑块的加速度变化,这些生物行为特征很难被脚本完美模拟。
部署交互式验证时,不建议对所有流量一律弹出验证码,这会严重伤害用户体验。应该设置一个触发阈值,例如当某个IP或会话在单位时间内的请求量超过正常值,或者请求的URL集中在高消耗的动态接口上时,才动态插入验证码挑战。验证通过后,可以在服务器端对该客户端特征进行短期白名单标记。客户端特征可以综合IP前缀、User-Agent、屏幕分辨率等信息生成一个设备指纹,避免单纯依赖IP导致误封。
对于自主开发能力较强的团队,可以自己实现一套简化的行为验证逻辑。比如生成一个滑块缺口图,要求用户拖动滑块到指定位置。前端收集鼠标按下、移动、松开的时间序列数据,加密后连同滑动距离一起提交到后端。后端不仅校验距离是否匹配,还分析时间序列数据是否符合人类操作的特征,比如是否存在加速和减速过程,是否有不自然的匀速直线运动。第三方服务虽然方便,但自研方案可以完全掌控数据流向和验证逻辑,避免外部依赖带来的延迟和隐私风险。
利用TCP层和TLS层的指纹验证更隐蔽的验证发生在连接建立阶段。不同的操作系统和浏览器在TCP初始窗口大小、TCP选项顺序、TLS握手时的密码套件列表、椭圆曲线参数等方面都存在细微差异,这些差异组合起来就形成了客户端指纹。JA3和JA4就是基于TLS Client Hello报文的指纹算法。攻击工具即使能执行JavaScript,其底层网络库发出的TLS指纹也往往与真实浏览器不同。
在反向代理或负载均衡器上,可以实时计算每个入站连接的JA3指纹,并与已知的真实浏览器指纹库进行比对。如果指纹匹配已知的恶意工具特征,或者不在主流浏览器的白名单范围内,可以直接在TLS握手阶段就丢弃连接,或者返回一个要求重试的警报页面。这种验证对用户完全透明,不需要任何前端代码配合,而且攻击者修改TLS指纹的技术成本远高于修改HTTP请求头。
Nginx上可以通过集成nginx-module-njs来获取TLS握手信息并计算JA3指纹。以下是一个简化的njs脚本片段,用于在TLS握手时检查指纹:
function check_ja3(s) {
var ja3 = s.variables.ssl_ja3;
var whitelist = ['e0c9e0d1c9e0d1c9e0d1c9e0d1c9e0', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'];
if (whitelist.indexOf(ja3) == -1) {
s.return(403, "Access Denied: Unrecognized client fingerprint");
}
}
需要注意的是,JA3指纹库需要持续维护,因为浏览器版本更新会导致指纹变化。建议只对触发频率异常的IP开启指纹验证,或者将指纹作为风险评分的一个维度,而不是唯一的拦截依据。结合请求速率、URL访问模式等多维特征综合判断,可以大幅降低误杀率。
基于请求速率的动态令牌验证还有一种轻量级的验证思路,是给每个客户端的请求序列插入动态变化的令牌参数。服务器在响应中携带一个下次请求必须包含的令牌,这个令牌由服务器根据当前请求的序列号和时间窗口计算得出。正常浏览器会按照页面资源的依赖顺序依次发起请求,每个请求都能携带正确的令牌。而CC攻击通常是并发大量请求,或者只针对单一URL反复请求,无法维护正确的令牌序列。
具体做法是,在用户首次访问时,服务器生成一个初始种子,加密后写入Cookie。后续每个请求,客户端需要从上一个响应中提取下一次请求应使用的令牌值。如果服务器发现某个请求的令牌与预期不符,或者令牌已经被使用过,则直接丢弃该请求,并返回一个要求重新同步的响应。这种机制相当于在应用层实现了一个简单的状态机,攻击者必须完整解析每个响应并正确构造下一个请求,这极大地增加了攻击脚本的编写难度。
为了兼容CDN和缓存,令牌的传递可以放在自定义HTTP头中,而不是URL参数里,这样静态资源仍然可以被边缘节点缓存。对于API接口,可以要求客户端在请求头中携带X-Request-Token,其值由上一次API响应的X-Next-Token头提供。首次请求时该字段为空,服务器会返回一个初始令牌和短暂的冷却时间。这种设计对前端单页应用也很友好,只需要在拦截器中统一处理即可。
多层级验证的组合编排单一的验证手段总有被攻破的可能,真正稳健的防护体系是分层组合的。第一层在网络边缘,通过IP信誉库和速率限制丢弃明显恶意的请求。第二层是TCP/TLS指纹验证,拦截非浏览器特征的连接。第三层是JavaScript跳转验证,过滤掉不支持JS的脚本。第四层是动态令牌验证,打乱攻击者的请求序列。最后一层才是交互式验证码,作为兜底手段应对高级攻击。
每一层验证都可以配置不同的触发条件,避免对所有流量一视同仁。例如,对于来自数据中心IP段的请求,直接进入更严格的验证流程;对于历史行为良好的IP段,可以放宽验证要求。验证状态也应该在各级代理之间共享,可以通过共享内存或Redis来存储验证通过的客户端标识,避免用户每经过一层代理都要重新验证。
在实施这些验证步骤时,务必监控验证通过率和延迟指标。如果验证通过率突然下降,可能是正常用户受到了影响,需要检查验证逻辑是否误伤了某些合法的浏览器或网络环境。如果验证延迟过高,说明验证流程本身成为了瓶颈,需要优化令牌生成算法或增加验证服务的处理能力。验证的目的是恢复服务,而不是制造另一个故障点。
验证步骤对SEO和用户体验的兼顾搜索引擎的爬虫在抓取页面时,通常无法执行复杂的JavaScript,也不会进行交互式验证。如果验证逻辑不加区分地对所有请求生效,爬虫抓取到的将是一个空白页面或验证页面,导致网站被降权。因此,验证系统必须能够识别主流搜索引擎爬虫的User-Agent和IP段,对它们直接放行。同时,也可以通过反向DNS查询来验证爬虫身份的真实性,防止攻击者伪造搜索引擎的User-Agent绕过验证。
对于真实用户,验证流程应该尽可能做到无感知。JavaScript跳转验证如果代码精简,执行时间可以控制在100毫秒以内,用户甚至注意不到页面发生过一次瞬间跳转。动态令牌验证完全在HTTP层面完成,对页面渲染没有任何影响。只有在极端情况下才弹出验证码,并且验证码的设计应该简洁友好,一次点击即可完成,避免要求用户识别模糊的图片文字。
当攻击流量逐渐消退后,应该逐步降低验证强度,恢复到正常访问模式。可以设置自动降级策略,当全网请求量下降到预设阈值以下,且持续一段时间后,自动关闭交互式验证,仅保留基础的速率限制。这样既能保证在攻击期间服务不中断,又不会长期影响用户访问体验和搜索引擎收录。
增加验证步骤的本质,是用计算资源换取安全防护。每一层验证都在消耗攻击者的计算资源和时间,同时尽可能少地消耗服务器资源和用户耐心。合理设计和组合这些验证机制,可以让被CC攻击的网站在不增加大量硬件成本的前提下,快速恢复对正常用户的服务能力。关键是持续观察攻击手法的变化,动态调整验证策略,让防护始终比攻击快一步。
