静态资源站点遭遇CC攻击时,传统WAF和全站JS跳转方案往往带来两个极端:要么防护规则过重导致正常大流量访问被误伤,要么验证逻辑对纯静态图片、CSS、JS文件完全失效。真正有效的轻量化方案,核心思路是把“人机识别”从应用层下沉到网络边缘,同时利用静态文件本身可缓存、无状态、URI固定的特性,在边缘节点完成绝大部分攻击流量的过滤,源站只接收经过验证的TCP连接和HTTP请求。下面直接拆解可落地的四层防护结构。
第一层:利用TCP连接行为做首轮过滤CC攻击不管伪装成什么浏览器,最终都要建立TCP连接。静态资源站点的优势在于正常用户请求的文件类型极其固定,连接行为有规律可循。在Nginx或边缘节点上开启连接限速和并发限制,能直接挡掉一半以上的低效攻击。具体配置思路是限制单IP的并发连接数、新建连接速率,并对超过阈值的连接直接返回444或静默断开。比如正常浏览一个图片站,单IP并发连接很少超过20,而CC攻击工具通常一口气建立几百上千个连接。下面这段Nginx配置可以直接用在静态资源站点的server块中:
limit_conn_zone $binary_remote_addr zone=static_conn:10m;
limit_req_zone $binary_remote_addr zone=static_req:10m rate=50r/s;
server {
listen 80;
server_name static.example.com;
limit_conn static_conn 20;
limit_req zone=static_req burst=100 nodelay;
location / {
root /var/www/static;
try_files $uri $uri/ =404;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
这里的rate=50r/s和burst=100是针对静态资源请求频率较高的场景设置的宽松值,实际部署时需要根据站点日志中单IP的峰值请求速率来调整。关键点是limit_conn限制的是并发连接数,limit_req限制的是请求速率,两者叠加后,攻击工具即使伪造了User-Agent,也会因为连接行为异常被直接断开,根本到不了后面的验证环节。
第二层:边缘节点缓存与回源隔离静态资源站点最容易被忽略的防护手段,就是把边缘缓存当成“盾牌”。如果源站前面有CDN或自建的反向代理缓存层,那么CC攻击的请求只要命中缓存,就不会对源站产生任何压力。轻量化方案的核心原则是:让边缘节点承担100%的静态文件响应,源站只处理缓存未命中的少量请求。要实现这个目标,需要做两件事。第一,给所有静态文件设置足够长的缓存时间,并且使用immutable指令告诉浏览器和中间缓存节点该文件不会变化。第二,在边缘节点上配置缓存锁定机制,当某个URI正在回源时,其他相同URI的请求直接等待或返回缓存中的旧版本,避免攻击者利用大量不存在的URI强制回源。Nginx的proxy_cache_lock指令就是干这个用的:
proxy_cache_path /tmp/static_cache levels=1:2 keys_zone=static_cache:100m max_size=10g inactive=7d use_temp_path=off;
server {
listen 80;
server_name static.example.com;
location / {
proxy_pass http://origin_server;
proxy_cache static_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 30d;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
}
}
proxy_cache_lock开启后,当同一个URI的多个请求同时到达且缓存未命中时,只有第一个请求会回源,其余请求等待锁释放后直接从缓存取。这直接废掉了CC攻击中常见的“随机参数轰炸”手法,因为攻击者构造的大量带随机查询参数的URI,在缓存层就会被锁住,源站根本收不到这些请求。同时proxy_cache_use_stale保证了即使源站短暂不可用,边缘节点也能继续用旧缓存响应,把攻击影响降到最低。
第三层:轻量级JavaScript验证,只对可疑流量启用全站JS跳转对静态资源站点来说太重了,图片、CSS、JS文件根本不需要执行JavaScript,强制跳转只会拖慢正常访问速度。轻量化的做法是“条件性验证”:只有当请求触发了前面两层的阈值规则时,才对这个IP或这个会话插入一次JS验证。验证逻辑非常简单,客户端执行一段JS计算出特定值,然后带着这个值重新请求一次,边缘节点验证通过后放行。这段验证代码可以直接用Nginx的Lua模块实现,不需要引入额外的验证服务:
location / {
access_by_lua_block {
local ip = ngx.var.binary_remote_addr
local conn_count = ngx.shared.conn_limit:get(ip) or 0
if conn_count > 15 then
local token = ngx.var.cookie_static_token
if not token or token ~= ngx.md5(ip .. ngx.var.http_user_agent .. "static_salt") then
ngx.header.content_type = "text/html"
ngx.say([[]])
return ngx.exit(200)
end
end
}
root /var/www/static;
}
这段逻辑只对并发连接数超过15的IP启用JS验证,正常用户根本感知不到。验证通过后写入一个cookie,后续请求带着这个cookie直接放行。攻击工具如果只是简单的HTTP客户端,不会执行JS,自然拿不到cookie,后续请求全部被拦在验证页。这里的关键设计是验证阈值要设得比第一层的硬限制宽松,让正常用户在偶尔突发请求时不会被误伤,而攻击工具的高并发特征一定会触发验证。
第四层:URI白名单与文件类型锁定静态资源站点的请求URI应该高度可预测。如果一个图片站的请求中出现了.php、.asp、.jsp后缀,或者URI中包含大量随机字符串,那基本可以判定为攻击流量。轻量化方案是在边缘节点上配置严格的URI白名单,只允许特定目录和特定文件扩展名的请求通过,其他请求直接返回403或444。Nginx的location匹配可以精确到文件扩展名:
location ~* \.(jpg|jpeg|png|gif|webp|svg|css|js|woff|woff2|ttf|eot|ico|mp4|webm|pdf)$ {
root /var/www/static;
expires 30d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location / {
return 403;
}
这段配置直接拒绝了所有不在白名单内的请求。攻击者如果试图用随机URI打穿缓存,或者扫描站点漏洞,请求会直接被403拦截,连回源的机会都没有。对于确实需要动态响应的场景,比如统计接口或验证接口,可以单独配置一个location,并加上更严格的访问控制。文件类型锁定的另一个好处是减少了边缘节点上正则匹配的开销,Nginx处理静态文件的性能会更好。
方案组合与阈值调优四层防护不是各自为战,而是按顺序叠加:TCP连接行为过滤先挡掉最粗暴的攻击,边缘缓存锁定防止缓存穿透,条件性JS验证处理漏网的高并发IP,URI白名单兜底拦截异常请求。实际部署时,阈值的设定要基于站点日志而不是拍脑袋。建议先采集一周的正常访问数据,统计单IP的并发连接数分布、请求速率分布、URI模式分布,然后把阈值设在正常峰值的1.5到2倍位置。比如日志显示正常用户单IP并发连接数最高是12,那limit_conn就设为20或25;正常请求速率最高是30r/s,那limit_req的rate就设为50r/s。这样既能保证正常用户不受影响,又能让攻击工具的异常行为暴露出来。
对于流量特别大的静态资源站点,还可以把第一层的连接限制放到内核层面处理,用iptables的hashlimit模块或者SYN cookie机制来扛住海量连接。但轻量化方案的原则是尽量不引入额外组件,用Nginx本身的能力解决大部分问题。如果攻击流量超过了单台边缘节点的处理能力,横向扩展边缘节点比堆砌复杂的防护软件更有效,因为静态资源站点的防护逻辑本身就是无状态的,任何一台边缘节点都能独立完成验证和过滤。
这套方案的核心思想是“用静态站点的特性来防护静态站点”。静态文件可缓存、URI固定、请求行为简单,这些特性本身就是最好的防护依据。与其部署一套重型的WAF规则引擎去分析每个请求的内容,不如在边缘层利用连接行为、缓存机制和简单的JS验证把攻击流量挡在源站之外。对于绝大多数面向公开互联网的静态资源站点来说,这种轻量化方案的实际防护效果往往比复杂的全站防护系统更好,因为误伤率更低,响应速度更快,运维成本也更低。
