本节摘要:本节做一次纯认知性的拆解:站点凭什么认出一次自动化访问。答案是被动信号的综合判定,三类信号逐一看清:浏览器指纹(UA、Canvas 与 WebGL 渲染特征、字体枚举、屏幕与硬件参数——组合起来足以在无 Cookie 的情况下唯一标识一个环境)、headless 特征(navigator.webdriver 标记、调试协议痕迹、无头模式的环境缺项——「这不是一个有人在用的浏览器」的信号)、行为特征(鼠标轨迹的物理性、滚动与输入的节奏分布——「这不是一个人的操作节奏」的信号)。Cloudflare 类挑战页是这些信号综合判定的产品形态。本节的立场从标题就开始强调:被识别不等于该绕过——识别之后的正确动作序列是自查授权范围、自查速率、检查身份呈现方式,而不是升级对抗;本章所有内容以此为前提。示例是在自有页面上读取自身指纹面的双语代码,用于理解机制,不用于任何对抗。细节以官方文档与公开研究为准。
站点认识你(或者说认出「这不是正常用户」)靠的不是单一开关,是三类信号的加权投票:
一次访问在站点侧留下的信号 ① 环境指纹(这个浏览器环境是谁) UA、Canvas/WebGL 渲染特征、字体清单、屏幕与硬件参数 ——组合熵极高,无 Cookie 也能唯一标识环境 ② headless 特征(这是不是一个有人在用的浏览器) webdriver 标记、调试协议痕迹、缺插件、缺交互历史 ③ 行为特征(这个操作像不像人) 鼠标轨迹的物理性、滚动节奏、输入间隔的分布 ——Agent 的动作天然「太直、太快、太规律」
三类的可检测性不同:环境指纹最稳定(环境不变信号不变),行为特征最难伪装(要模拟的不是一次动作而是统计分布)。Cloudflare 类挑战页的常见形态——静态质询、行为观察、失败后升级质询——本质就是先看①②、存疑看③、再存疑升级对抗强度。理解这个分层有助于定位自己「为什么被拦」:环境信号脏(无头特征一堆)与节奏信号脏(毫秒级连续点击)是两种病,后者发生在代码层,前者发生在环境层。
| 维度 | 采集面 | 说明 |
|---|---|---|
| UA 与平台 | navigator.userAgent、platform | 版本与操作系统声明,最易读也最易假 |
| Canvas 渲染 | 同一绘图指令的像素输出 | 渲染管线的微小差异形成稳定特征 |
| WebGL | 渲染器与厂商字符串、渲染结果 | 显卡与驱动指纹 |
| 字体清单 | 可用字体探测结果 | 装机差异的组合特征 |
| 屏幕与硬件 | 分辨率、色深、硬件并发数、内存档位 | 环境参数组 |
单独每一维都不唯一,组合起来熵极高——这正是「指纹」这个名字的来源:单项特征人人相似,全套特征一眼认人。工程含义有二:其一,环境一致性是会话管理的刚需(第 7.3 节托管平台把它列为供给价值);其二,指纹会说话——一个声明 macOS 却渲染出 Windows 字体集的环境,比「无头」本身更扎眼。自相矛盾比单调更可疑,这是 8.2 节环境一致性话题的伏笔。
headless(无头)模式是「服务器在跑浏览器」的形态,留下一组特征性信号:navigator.webdriver 为真、自动化调试协议的痕迹、典型缺项(插件、交互历史、通知权限从未被问过)、以及无头 UA 里的历史标记。这些信号单独都不致命,组合起来就是「非人类环境」的高置信判定。
行为特征更根本:人类的操作有物理性与统计分布——鼠标轨迹有加速减速与过冲、滚动有间歇、输入间隔不均匀;脚本与模型驱动的动作则「太完美」:直线轨迹、固定间隔、毫秒级反应。第 5 章视觉流派的坐标点击、第 1 章的元素直击,在这个维度上天然处于劣势。需要说明的是:行为特征检测对授权场景同样有意义——你在自己站点上做压测或回归时,也应该让流量带上「这是自动化」的可识别性(速率与标识,见 8.2 的身份呈现),而不是把自动化流量伪装成用户流量去污染自己的分析数据。
核心立场展开。「被识别」的直接表现通常是:质询页出现、请求被拒、功能降级。这时正确的动作序列:
被拦下 │ ▼ ① 自查授权:这个站点、这个账号、这个操作在授权范围内吗? └─ 否 ──▶ 停止。问题不在技术,在任务本身不该存在 │ 是 ▼ ② 自查速率:访问频率与方式站点方可接受吗?(服务条款、robots 说明) └─ 否 ──▶ 降到可接受水平;重新设计任务节奏 │ 是 ▼ ③ 检查身份呈现:自动化身份是否如实标识?(UA 声明、官方 API 优先) └─ 是仍有质询 ──▶ 走站点提供的验证通道或联系站方,说明正当用途
注意这个序列里没有「换个指纹再试」这一步。绕过质询在技术上存在可能性,但它在三个层面都站不住:授权层面(质询是站点明示的意愿表达,绕过即违背);法律层面(多法域下规避技术措施有专门风险);伦理层面(你不希望自己系统的流量是对抗性的)。本书不演示任何绕过验证码、付费墙、质询页的做法——这条红线在 8.2 节还会再划一次,并配上滥用场景的点名批判。
理解机制最好的办法是看数据。在自有页面上把自己的环境信号读出来(about.html 即可):
# fingerprint_surface.py —— 在自有页面读取环境信号(写法示意,以官方文档为准) from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("http://127.0.0.1:8080/about.html") surface = page.evaluate( """() => ({ ua: navigator.userAgent, platform: navigator.platform, webdriver: navigator.webdriver, language: navigator.language, concurrency: navigator.hardwareConcurrency, screen: [screen.width, screen.height, screen.colorDepth], touch: navigator.maxTouchPoints, })""" ) for key, value in surface.items(): print(f"{key}: {value}") browser.close()
// fingerprint_surface.mjs —— 在自有页面读取环境信号(写法示意,以官方文档为准) import { chromium } from "playwright"; const page = await (await chromium.launch()).newPage(); await page.goto("http://127.0.0.1:8080/about.html"); const surface = await page.evaluate(() => ({ ua: navigator.userAgent, platform: navigator.platform, webdriver: navigator.webdriver, language: navigator.language, concurrency: navigator.hardwareConcurrency, screen: [screen.width, screen.height, screen.colorDepth], touch: navigator.maxTouchPoints, })); console.log(JSON.stringify(surface, null, 2));
用有头与无头两种模式各跑一次并对比输出:webdriver 字段翻转、UA 尾部差异、硬件参数不同——这就是同一份代码在两种环境下的「检测面差」。做这件事的价值不在对抗(信号就在那里),在于知道自己环境的信号画像:它会解释很多现象(为什么本地调试一切正常、上了无头池就频繁被质询——环境信号变了)。Canvas 与 WebGL 特征的读取同理,只是采集面在渲染层,思路一致,不展开。
| 坑 | 现象 | 排查与处理 |
|---|---|---|
| 把被拦当 bug | 反复重试触发更强质询 | 先走第四节动作序列,自查授权与速率 |
| 环境自相矛盾 | UA 说 A、字体说 B | 环境一致性配置(8.2 节),别东拼西凑 |
| 无头池直接上生产 | 质询率骤增 | 对照第五节方法看环境信号差,评估供给(7.3) |
| 在别人站点「测指纹」 | 拿检测页当靶场练手 | 越界——只在自己页面上做实验 |
| 伪装思路先入为主 | 遇事就想 stealth | 重读第四节:绕过不在正确动作序列里 |
认识了检测面,下一节正面回答那个悬着的问题:stealth 类技术到底是什么、什么时候是正当工程手段、什么时候是红线——以及黄牛、刷量、爬隐私这些滥用场景,为什么要逐个点名批判。