爬虫请求头哪里露馅?头部顺序也是指纹

爬虫请求头不只有 UA 可看。头部的先后顺序、有没有 Sec-Fetch 系列、Accept 写了什么,都是脚本很难伪装的部分。本文讲清怎么读、怎么判、怎么避免误伤。

Q:爬虫请求头为什么比 UA 更能说明问题?

  • UA 是一个字符串,改一行代码就能换。头部的顺序、集合、大小写是 HTTP 客户端库写死的,改起来要重写发送逻辑。
  • 顺序指纹在 HTTP/1.1 和 HTTP/2 上都成立,而且不需要执行 JS,服务端看报文就能判。对比 Canvas 指纹要跑脚本、TLS 指纹要解密,它的成本低得多。
  • 和另外三层配合起来才完整:网络层看 IP 与 ASN,传输层看 TLS 与 JA3,报文层看头部序列,应用层看 JS 指纹与行为节奏。
  • 它最适合做「降噪」:把十万个请求筛到几千个可疑,再上验证码,成本才降得下来。

Q:真实浏览器发出来的头部是什么样?

  • Chrome 桌面端的典型顺序是:Host → Connection → sec-ch-ua → sec-ch-ua-mobile → sec-ch-ua-platform → Upgrade-Insecure-Requests → User-Agent → Accept → Sec-Fetch-Site → Sec-Fetch-Mode → Sec-Fetch-User → Sec-Fetch-Dest → Accept-Encoding → Accept-Language → Cookie。
  • 三个要点:三个 sec-ch-ua(Chrome 从 89 起带)、四个 Sec-Fetch(从 76 起带)、Accept 的默认值是一长串 text/html,application/xhtml+xml,application/xml;q=0.9,...。
  • 脚本作者的省事写法是 Accept 直接给 */*,Accept-Language 干脆不带,这两点几乎是脚本的默认签名。
  • 顺序在同一浏览器的大版本内基本不变,跨大版本会调整,所以基线要按版本维护。

Q:常见脚本库的默认头部顺序差在哪?

  • Python requests:Host → User-Agent → Accept-Encoding → Accept → Connection,只有五个头,Accept-Encoding 出现在 Accept 之前,与浏览器相反。
  • Go net/http:Host → User-Agent → Accept-Encoding,通常就三个。
  • curl:Host → User-Agent → Accept,同样只有三个,Accept-Encoding 除非手动加。
  • Node 的 fetch:Host → Connection → 少量默认头,sec-fetch 系列一个不带。
  • 一眼能看出的破绽有两个:头太少、Accept-Encoding 排在 Accept 前面。真实浏览器一定是 Accept 先、Accept-Encoding 后。
# 三行命令看清某个客户端到底发了什么顺序
curl -sv https://example.com/ -o /dev/null 2>&1 | sed -n '/^> /p'
python3 -c "import requests;print(requests.get('https://example.com').request.headers)"

Q:Nginx 能拿到头部顺序吗?

  • 不能。$http_xxx 变量和 ngx.req.get_headers() 拿到的都是无序的哈希表,顺序在这一步已经丢了。
  • 唯一的办法是读原始报文:ngx.req.raw_header(),自己按行切出来。
  • 注意两个坑:raw_header 有大小限制,Cookie 很长时会被截断;它返回的是解码后的字符串,HTTP/2 下伪头部已经转成普通头。
location / {
    access_by_lua_block {
        local raw = ngx.req.raw_header()
        local seq = {}
        for line in raw:gmatch("[^\r\n]+") do
            local name = line:match("^([%w%-]+):")
            if name and name ~= "Host" then
                seq[#seq + 1] = name:lower()
            end
        end
        ngx.var.hdr_seq = table.concat(seq, "|")
    }
    add_header X-Hdr-Seq $hdr_seq always;
}
  • 第一周只记录不判定。把这个序列写进访问日志,离线跑一天,你会看到几十种组合,其中占比极小的那些就是机器人。
  • 原生 Nginx 没有 Lua 的,把序列交给 WAF 或边缘做,规则里一般自带请求头顺序检测。
log_format hdr '$remote_addr - [$time_local] "$request" $status '
                '$http_user_agent "$http_accept" "$hdr_seq"';
access_log /var/log/nginx/hdr.log hdr;

Q:HTTP/2 下的头部指纹为什么更硬?

  • h2 的伪头部顺序(:method、:authority、:scheme、:path 的先后)由客户端库固定,浏览器几乎完全一致。
  • 再加上 SETTINGS 帧的六个参数(HEADER_TABLE_SIZE、ENABLE_PUSH、MAX_CONCURRENT_STREAMS、INITIAL_WINDOW_SIZE、MAX_FRAME_SIZE、MAX_HEADER_LIST_SIZE)、WINDOW_UPDATE 的增量、优先级帧的有无,就构成了完整的 h2 指纹。
  • 脚本站这一侧差异极大:Go 的 SETTINGS 和 curl、requests 都不一样,而且改动要重写库。这是目前性价比最高的一层。
  • 局限:源站 Nginx 拿不到 SETTINGS,必须在边缘(CDN、WAF、自建网关)或解密后的抓包里取。

Q:怎么判定「顺序和声称的 UA 不匹配」?

  • 三类规则,按可靠性从高到低:
  • 硬矛盾:UA 写 Chrome 120 却没有 sec-ch-ua、没有 Sec-Fetch-*。真 Chrome 不可能缺这两个,命中就是伪造。
  • 顺序偏差:与同 UA 家族基线的前五个头顺序不一致,特别是 Accept-Encoding 跑到了 Accept 前面。
  • 集合缺失:没有 Accept-Language、Accept 是 */*、Cookie 一个都没有却访问了几百个页面。
  • 至少两条同时命中才打标。单条不定罪,因为代理和杀毒软件会改写头部,容易误伤。
  • 落地成评分:每条 +1,2 分进观察名单,3 分进限速,4 分才上挑战。

Q:哪些正常流量会被头部顺序误伤?

  • 企业代理与杀毒软件的 SSL 中间人:会增删头、重排顺序,还可能插入自己的 X- 头。
  • 移动运营商透明代理:会改 Accept-Encoding,甚至重写 UA。
  • 老旧浏览器与嵌入式 WebView:没有 sec-ch-ua,Sec-Fetch 也不全,但确实是人在用。
  • 微信、QQ 内置浏览器:UA 特殊,头部集合比标准 Chrome 少。
  • CDN 回源:部分厂商会重写 Host、Connection,顺序跟着变,源站看到的不是原始客户端。
  • 结论:先看 Referer 分布、URL 集中度、会话深度,再看头部。单靠头部一条就封,是自找投诉。

Q:判定出来之后怎么处置?

  • 一级,打标加记录:响应头带一个内部评分,不拦,先把数据攒起来。
  • 二级,只对「打标 + 高频 + 没有 JS 指纹」的 IP 限速,返回 429 加 Retry-After,让它自己退避。
  • 三级,对内容页下发 JS 挑战或验证码,不要对首页下发,首页是 SEO 和转化的入口。
  • 四级,/24 聚合封禁,只对明显的住宅代理池才用,单 IP 封禁对它们无效。
  • 搜索引擎蜘蛛必须单独白名单,用官方公布的 IP 段加反查验证,别用头部特征去猜。
map $hdr_score $bot_key {
    default   "";
    "3"       $binary_remote_addr;
    "4"       $binary_remote_addr;
}

limit_req_zone $bot_key zone=botsuspect:10m rate=1r/s;

server {
    location / {
        limit_req zone=botsuspect burst=5 nodelay;
        limit_req_status 429;
        add_header Retry-After 60 always;
    }
}
  • 关键点:limit_req 的 key 为空字符串时规则不生效,所以只有被评分的 IP 受限,其余流量完全不受影响。白名单也靠这个特性实现。

Q:头部指纹会不会很快失效?

  • 会。脚本作者照着浏览器抓一次包,把顺序抄过去,成本很低。所以顺序只能当筛子,不能当墙。
  • 真正的门槛是组合:头部顺序 + TLS/h2 参数 + JS 指纹 + 行为节奏,四层里伪造三层已经很不划算。
  • 基线必须按月重算。浏览器大版本更新会改头顺序,Chrome 一年要改好几次,写死的规则半年就废。
  • 规则要做成配置表而不是硬编码,改阈值不该需要发版。

Q:还有什么补充建议?

  • 先记录两周再判定。你自己站点的真实分布,比任何通用规则都准。
  • 把「顺序异常」当降噪手段而不是拦截手段。它省的是验证码的成本,不是替代验证码。
  • 对声明过身份的 AI 爬虫(GPTBot、ClaudeBot 等)按官方说明放行,别用 UA 关键字一刀切,误拦会影响内容在这些平台的可发现性。
  • robots.txt 是约定不是执行,真正落地靠这一层的判定与限速,两者要一起用。