高频爬虫和CC攻击在行为特征上已经高度重叠,很多人分不清自己服务器日志里那堆请求到底是竞争对手在抓数据还是黑客在打流量。直接说结论:如果你看到同一IP或不同IP在短时间内以固定频率访问同一个消耗资源的接口,比如搜索、列表翻页、动态渲染页面,而且请求间隔精确到毫秒级,这大概率不是普通爬虫,而是带有明确资源消耗意图的CC攻击。普通爬虫会尽量伪装成浏览器,请求间隔随机化,而攻击性流量往往不做这种伪装,甚至故意用极快频率去触发数据库查询、PHP运算或API调用。

从日志特征识别高频爬虫与CC攻击

先看Nginx或Apache日志里几个关键字段:请求时间戳、请求URI、状态码、请求耗时、User-Agent和真实IP。高频爬虫最明显的特征是请求间隔极度均匀,比如每100毫秒一个请求,持续几小时。这种节奏感是脚本写死的,人类用户和正常搜索引擎爬虫做不到。用命令行快速排查:

awk '{print $4}' access.log | cut -d: -f2,3 | sort | uniq -c | sort -rn | head -20

这条命令会统计每秒内的请求数分布。如果某个秒级窗口内请求数异常集中,立刻定位到对应时间段,再拉出该时间段内请求频率最高的IP:

awk '$4 ~ /01\/Apr\/2025:14:23/' access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

高频爬虫还有一个明显特征:User-Agent要么是空值,要么是过时的浏览器标识,要么清一色用同一个UA。正常爬虫会轮换UA池,攻击脚本往往懒得改。另外看请求URI的分布,如果某个IP只盯着一个动态页面反复请求,参数都不带变的,那就是典型的CC攻击模式,目的是耗尽服务器CPU或数据库连接池。

基于Nginx的实时拦截策略

Nginx层面做拦截比应用层高效得多,因为不需要把请求传递到PHP或后端服务就能直接返回403或444。最常用的两个模块是ngx_http_limit_req_module和ngx_http_limit_conn_module。limit_req用来限制请求速率,limit_conn用来限制并发连接数。配置示例如下:

http {
    limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
    limit_req_zone $server_name zone=perserver:10m rate=100r/s;
    
    server {
        location /search/ {
            limit_req zone=perip burst=20 nodelay;
            limit_req zone=perserver burst=200 nodelay;
        }
    }
}

这里rate=10r/s表示每秒允许10个请求,burst=20是允许瞬间突发的额外请求数,nodelay表示超出burst立即返回503而不排队。对于搜索接口这种高消耗页面,rate可以设得更低,比如2r/s。关键是要把limit_req_zone的key设置精准,用$binary_remote_addr比直接用$remote_addr节省内存,10m的空间大约能存储16万个IP的记录。

但limit_req有个盲区:如果攻击者使用大量代理IP,每个IP的请求频率都不高,但总量巨大,单IP限速就失效了。这时候需要结合$server_name做全站限速,或者用更高级的key组合,比如把$binary_remote_addr和$request_uri拼接,针对特定接口做更细粒度的控制:

limit_req_zone $binary_remote_addr$request_uri zone=peruri:10m rate=5r/s;

这样同一个IP对同一个URI的请求频率被限制,不影响它访问其他页面。对于爬虫喜欢翻页的行为,这个策略非常有效。

利用Lua脚本实现动态挑战验证

静态限速规则容易被绕过,因为攻击者可以调整频率刚好卡在阈值之下。更高级的做法是用OpenResty或Nginx的Lua模块在请求到达时注入JavaScript挑战。原理是:当某个IP的请求频率触发阈值后,不直接返回403,而是返回一段需要浏览器执行JavaScript才能跳转的页面。合法用户的浏览器会自动执行JS并携带计算结果重新请求,而脚本爬虫通常不解析JS,直接被拦截。

具体实现可以用Nginx的access_by_lua阶段写逻辑:

access_by_lua_block {
    local redis = require "resty.redis"
    local red = redis:new()
    red:connect("127.0.0.1", 6379)
    
    local ip = ngx.var.binary_remote_addr
    local count = red:incr("rate:" .. ip)
    if count == 1 then
        red:expire("rate:" .. ip, 1)
    end
    
    if count > 20 then
        local cookie = ngx.var.cookie_verify
        if not cookie or cookie ~= red:get("token:" .. ip) then
            ngx.header.content_type = "text/html"
            ngx.say([[]])
            ngx.exit(ngx.OK)
            return
        end
    end
}

这段逻辑把IP请求计数存在Redis里,超过阈值就检查Cookie里是否有验证令牌。没有令牌就返回JS挑战页面,用户浏览器执行JS后获取令牌写入Cookie,再次请求时通过验证。脚本爬虫执行不了JS,请求链就断了。这个方案比直接封IP更优雅,不会误伤正常用户,而且攻击者要绕过必须模拟完整浏览器环境,成本大幅提高。

应用层指纹识别与行为分析

有些高级爬虫会使用Headless Chrome或Puppeteer,能执行JS,能保存Cookie,单从网络层面很难区分。这时候需要在应用层做浏览器指纹检测。几个有效的检测点:

第一,检测navigator.webdriver属性。正常浏览器这个值是false或undefined,而通过自动化工具启动的浏览器这个值是true。在页面里嵌入一段JS把这个值通过AJAX上报到后端,后端在会话中标记这个用户的可信度。

第二,检测Canvas指纹和WebGL指纹。爬虫工具通常会为了性能关闭GPU渲染,导致WebGL渲染器信息异常。收集这些指纹并与已知爬虫指纹库比对,可以识别出大部分自动化工具。

第三,行为轨迹分析。正常用户在页面上的鼠标移动轨迹、点击位置、滚动速度都有随机性,而脚本操作要么没有这些事件,要么轨迹过于机械。前端埋点收集这些行为数据,后端用简单的统计学方法就能区分。比如计算鼠标移动的加速度方差,人类用户的方差远大于脚本。

第四,请求头顺序检测。不同浏览器发送HTTP请求头时字段顺序是固定的,比如Chrome的请求头顺序是Host、Connection、Accept、User-Agent、Accept-Encoding等。很多爬虫框架用编程语言HTTP库发请求,头字段顺序跟浏览器完全不同。Nginx里可以用$http_变量组合记录头顺序,或者直接在应用层读取$_SERVER里头的原始顺序做比对。

利用WAF规则精准阻断

如果已经部署了ModSecurity或类似WAF,可以写自定义规则来拦截高频爬虫。ModSecurity支持基于IP的计数器,配合SecAction和deprecatevar指令能实现类似Nginx limit_req的效果,但优势在于可以结合请求内容做判断。比如一条规则同时检测请求频率和缺失Referer、缺失Accept-Language头:

SecRule IP:REQUEST_RATE "@gt 30" \
    "chain,phase:1,id:100001,deny,status:429,log,msg:'High frequency crawler detected'"
    SecRule &REQUEST_HEADERS:Referer "@eq 0" \
    "chain"
    SecRule &REQUEST_HEADERS:Accept-Language "@eq 0"

这条规则的意思是:如果某个IP的请求速率超过30次每秒,同时请求中没有Referer头也没有Accept-Language头,就返回429状态码。正常浏览器一定会带这两个头,而脚本爬虫经常省略。还可以加入对特定URI模式的匹配,比如对搜索接口、API接口重点监控。

反向代理与流量清洗架构

当攻击流量大到单台服务器Nginx都扛不住的时候,需要在网络层做清洗。自建清洗架构的核心思路是用高性能反向代理层做第一道过滤,只把清洗后的正常流量转发到源站。常用的组合是LVS+DPDK做四层负载均衡,后面接Nginx集群做七层过滤,再后面才是应用服务器。

四层过滤可以基于IP信誉库直接丢弃已知恶意IP的SYN包,用ipset配合iptables实现:

ipset create blacklist hash:ip timeout 86400
iptables -I INPUT -m set --match-set blacklist src -j DROP

然后写一个定时脚本从公开威胁情报源拉取恶意IP列表,批量加入ipset。也可以从自己的Nginx日志里分析出高频IP,自动加入黑名单。这个脚本每5分钟跑一次,把过去5分钟内请求数超过阈值的IP提取出来:

awk -v d1="$(date -d '5 minutes ago' '+%d/%b/%Y:%H:%M')" \
    '$4 > "["d1 {print $1}' access.log | sort | uniq -c | \
    awk '$1>500 {print $2}' | while read ip; do
    ipset add blacklist $ip
done

七层过滤则用前面提到的Nginx限速和Lua挑战。两层配合,四层挡掉已知恶意IP减轻七层压力,七层处理未知威胁并实时反馈到四层黑名单,形成闭环。

从爬虫角度理解规避技巧的本质

要防住高频爬虫,必须理解爬虫开发者常用的规避手段。最常见的规避方式是IP池轮换,用大量代理IP把请求分散,让单IP限速失效。针对这种策略,限速的key就不能只用IP,要结合设备指纹或会话ID。另一个规避手段是降低请求频率,把频率控制在阈值以下,然后拉长采集周期。这就要求阈值设置不能太死板,可以引入滑动窗口算法,检测一段时间内的总量而非瞬时速率。

爬虫还会模拟正常用户的浏览路径,先访问首页,再访问列表,再进入详情页,中间加上随机的停留时间。针对这种伪装,需要分析页面间的跳转逻辑是否合理。比如一个用户从列表页直接跳到了第10页,中间没有翻页动作,这在正常浏览中几乎不可能发生。把这种逻辑规则写入WAF或应用层中间件,能识别出大量模拟行为。

还有爬虫会解析验证码,用OCR或打码平台。传统的图形验证码已经基本失效,目前比较有效的是行为式验证,比如滑动拼图、点选文字,这些需要人类的空间认知能力,机器模拟成本高。但也不是绝对安全,所以更稳妥的做法是把验证作为最后手段,只在其他信号都指向可疑时才触发,避免影响正常用户体验。

说到底,高频爬虫和CC攻击的对抗是一场成本博弈。防御方的目标不是做到百分之百拦截,而是把攻击者的成本提高到超过其收益。综合运用Nginx限速、Lua挑战、浏览器指纹、WAF规则和流量清洗,让攻击者每获取一条数据都要付出更多代理IP、更多计算资源、更多时间,最终迫使其放弃。这套组合策略在实际生产环境中经过验证,能有效把恶意流量控制在可接受范围内,同时保持正常用户的访问体验不受影响。