网站怎么知道你是机器人
无头浏览器和自动化脚本会在 navigator、window 对象、DevTools 协议和网络层留下痕迹。这篇讲每条信号是怎么来的、为什么单独看都不可靠,以及正常人为什么也会被误判。
5 分钟阅读 · 发布于 2026年9月10日
你打开一个网站,还没点任何东西,Cloudflare 那个转圈的框已经决定放你过去了。它凭什么?答案是一堆各自都不太靠谱的小信号,加权之后变成一个够用的判断。本站的 Bot 检测卡片用的是同一类信号里最稳的几条,下面挨个说它们的来历。
三种结果
风控系统通常把访客分成三类。搜索引擎的爬虫(Googlebot、Bingbot、百度蜘蛛)和监控探针是"好机器人",它们公开自报家门,反向 DNS 能验证,站点一般欢迎。Selenium、Puppeteer、Playwright 驱动的脚本,以及冒充搜索引擎的爬虫,是"坏机器人"——刷号、撞库、薅羊毛、抓数据,站点想拦的是这些。剩下的是"疑似人类"。
注意措辞是"疑似"。没有任何信号能证明屏幕前坐着一个人,系统只能说"这个环境看起来不像被脚本控制"。
navigator.webdriver
这是最直接的一条。W3C WebDriver 规范要求:浏览器处于自动化控制下时,navigator.webdriver 必须为 true。Selenium、Playwright、Puppeteer 启动的浏览器默认都满足这条,所以检测代码只需要一行。
当然,做爬虫的人第一件事就是把它去掉。各种 stealth 插件会用 Object.defineProperty 覆盖它,或者在启动参数里禁掉。但去得不干净会留下新痕迹:Object.getOwnPropertyDescriptor(navigator, 'webdriver') 本来应该返回 undefined(属性定义在原型上),被覆盖后会返回一个描述符;重写过的 getter 调 toString() 得到的不再是 function get webdriver() { [native code] }。于是检测从"有没有这个标志"变成了"这个标志是不是被人动过"。
Chrome 独有的东西不见了
正常的桌面 Chrome 有几样东西几乎总在:window.chrome 对象(里面有 runtime、loadTimes 之类),navigator.plugins 里的三到五个内置插件(PDF Viewer、Chromium PDF Plugin 等),以及一个非空的 navigator.languages。
早期的无头 Chrome 缺 window.chrome,插件列表是空的,语言列表也可能是空的。UA 里干脆写着 HeadlessChrome,PhantomJS 时代更是直接自报名字。这些是最早一批检测手段,也是最早被修掉的——新版无头模式(--headless=new)已经和有头 Chrome 几乎无法区分了。
但插件数这条要小心:Chrome 移动端本来就是零插件,Firefox 早已不暴露插件列表。所以"插件数为 0"只在"桌面 Chrome 自称"这个前提下才算信号,否则纯属误报。本站真实性评分里这条只扣 5 分,就是这个原因。
窗口尺寸和权限的自相矛盾
无头环境没有真正的窗口,所以 window.outerWidth 和 outerHeight 常常是 0,或者和 innerWidth/innerHeight 的关系不合常理——比如内容区比外框还大,或者 screen.availWidth 大于 screen.width。真人用的浏览器不会这样。
另一个经典矛盾来自通知权限:Notification.permission 返回 denied,同时 navigator.permissions.query({name: 'notifications'}) 却说是 prompt。这两个 API 在正常浏览器里答案一致,只有在某些无头配置下才会打架。
CDP 痕迹
Puppeteer 和 Playwright 通过 Chrome DevTools Protocol 控制浏览器,而协议一旦挂上,就会有副作用。最有名的一种:DevTools 在序列化 Error 对象时会读取它的 stack 属性。检测脚本可以造一个 Error,给 stack 装一个 getter,然后把它扔进 console.debug()——没有 DevTools 时 getter 不会被触发,有 CDP 挂着时就会。类似地,Runtime.enable 会在页面里留下可以探测的执行上下文变化。
这类检测原理巧妙,但也是误报重灾区:你自己按 F12 打开开发者工具,一样会触发。所以本站只用其中最不容易出错的几条,而且明确标注"仅供参考"。反检测工具那边也在跟进——新版 Puppeteer 和一些补丁会避免 Runtime.enable,攻防在持续。
行为和网络:脚本之外的信号
到目前为止说的都是 JS 环境里的静态特征。真正的风控大头在另外两块。
先说行为。真人的鼠标轨迹是弯的、抖的,会 hover,会犹豫;脚本从 A 点直线到 B 点,点击间隔精确得像节拍器。键盘输入的节奏、滚动的加速度、页面停留时间,也都能拿来比。
再说网络。数据中心 IP(AWS、Google Cloud、各家 VPS)上跑着一个"普通用户",本身就不自然,参见IP 质量那篇。更狠的是 TLS 指纹:Python 的 requests 库握手时的 cipher suite 顺序、扩展列表(JA3/JA4 指纹)和 Chrome 完全不同,配上一个 Chrome 的 UA,在 TLS 层就露馅了,JS 都不用跑。
怎么把它们合起来
上面每一条单独拿出来都能找到反例。webdriver 可以被去掉,插件数可以被伪造,鼠标轨迹可以加噪声,住宅代理能换掉数据中心 IP。所以 Turnstile、reCAPTCHA v3、各家风控 SDK 做的不是逐条判断,而是给每条信号打权重、看信号之间是否互相印证,再加上大规模数据训练出来的模型,最后输出一个分数。分数高直接放行,中间挑战一下(转圈、点图片),低的拦掉。
这也是为什么"绕过检测"总是一场军备竞赛:修掉一条信号,其他信号的权重就上去了。
误报:被当成机器人的正常人
反过来,真人也会被冤枉,常见的就这么几种。
- 装了隐私扩展的人:扩展会改
navigator属性、屏蔽 canvas、清空插件列表,这些改动本身就像伪装。 - 企业电脑:组策略禁用插件、锁定语言、统一虚拟化环境,一批机器指纹完全相同。
- 老旧或小众浏览器,以及 Linux 桌面:样本少,模型见得少,不确定性高。
- 开着 DevTools 调试的开发者:CDP 类检测全部命中。
所以本站的检测卡片不给一个简单的"是/否",而是把触发了哪些信号一条条列出来。看到列表你就知道是哪个扩展、哪个设置惹的,不用对着一个红叉发愁。
常见问题
我不写爬虫,为什么要关心这个? 因为误报会落到你头上。装了隐私扩展、用着 VPN 出口、跑着 Linux,三条叠加就足够让不少站点频繁弹验证码。知道是哪条信号触发的,才能有针对性地调整。
把 navigator.webdriver 改掉就够了吗? 不够,而且改得不干净反而多了一条"被篡改"的信号。现代检测看的是整体一致性,不是某一个标志。
本站的 Bot 检测会不会把我的数据发到别处? 不会。所有检测在浏览器本地完成,只有你勾选参与统计时才会上传属性哈希,不含原始值。