蜜罐表单字段、陷阱链接和计时检查如何在不打扰真人的前提下抓住初级机器人:隐藏字段如何不伤及无障碍用户,为何自动填充是误判头号原因,以及被误拦时该如何自查。
蜜罐是网站能用的成本最低的机器人检测:在页面里放一个真人永远看不到、也够不到的东西,任何与它的互动都说明访问者是程序。它几乎不花成本,也不给真实用户添任何麻烦,而初级机器人只要把页面当作原始标记而非像素来读,就会立刻暴露。
核心要点
- 不对称性就是全部窍门。 真人操作的是渲染出来的内容;简单的机器人操作的是 DOM 里的内容。只存在于 DOM 中的陷阱就能把两者区分开。
- 三种常见陷阱: 必须保持为空的隐藏表单字段、指向
robots.txt已禁止的 URL 的隐藏链接,以及对表单提交速度的计时检查。 - 真正的工程难点是不伤及残障用户。 屏幕阅读器遍历的是 DOM,仅用 CSS 隐藏的陷阱可能会困住使用辅助技术的真人。
- 自动填充是误判的头号原因。 密码管理器和浏览器自动填充可能填上用户从没见过的字段,所以被拦下的真人通常并没有做错什么。
- 蜜罐是第一道过滤,不是完整防御。 它能抓住爬虫和表单垃圾脚本,对被自动化驱动的真实浏览器则毫无作用。
陷阱为什么有效
每一种机器人检测都在问同一个问题:这个客户端的行为像真人吗?多数答案需要分析,例如指纹一致性或行为评分。蜜罐把问题换成“这个客户端有没有碰过任何真人不可能碰到的东西?”,从而省掉了分析。
真人看到的是渲染后的页面,而表单垃圾脚本或爬虫通常读的是 HTML。它会找出每个 <input>,用看似合理的数据填满并提交。如果页面里有一个屏幕上看不见的输入框,脚本照样会填,服务器由此就能判断:提交者根本没有像真人那样“看”过这个页面。
表单字段蜜罐
经典陷阱是在表单里加一个视力正常的用户永远看不到的额外输入框——移出屏幕、设为零尺寸或设成完全透明——但它仍留在 DOM 里,而且关键在于:它仍会随表单一起提交。(disabled 的输入框根本不会被发送,这正是陷阱要“隐藏”而不是“禁用”的原因。)服务器端的规则很简单:只要这个字段带着内容提交过来,就拒绝该提交,可以静默拒绝,也可以返回通用错误。
三个设计细节决定它是否有效:
type="hidden"是最弱的版本。 隐藏输入本来就是用来携带值的,任何遍历表单的脚本都知道不去动它们,或原样带回。陷阱必须看起来像一个期待用户输入的字段。- 名称很重要。 叫
email2或website的字段看起来像脚本该填的东西,也像自动填充该填的东西。使用中性、无关的名称更不容易被意外填上。 - 判定必须放在服务器端。 用前端 JavaScript 判定的陷阱,客户端可以直接跳过;而蜜罐要抓的那些脚本,恰恰从来不执行页面的 JavaScript。
无障碍约束
大多数蜜罐都栽在这里。屏幕阅读器读的不是像素,而是遍历 DOM。仅仅移出屏幕的字段仍然会被朗读,仍可用 Tab 键聚焦,可能把键盘或辅助技术用户困在一个他们无法理解的字段里。
并不是所有 CSS 隐藏手法都一样,而差别恰恰就是问题所在。display: none 和 visibility: hidden 确实会把元素移出无障碍树和 Tab 顺序——但写得稍好一点的机器人在填字段前第一件事就是查这两个属性,所以实现者往往改用移出屏幕、零透明度或零尺寸。这些做法既让陷阱对脚本保持“有吸引力”,也让它对辅助技术完全暴露。
所以隐藏必须做两遍:一遍给眼睛,一遍给无障碍树。
- 在外层容器上使用
inert属性,可使整个子树不可交互、退出 Tab 顺序,并对辅助技术隐藏。 - 在不支持
inert的旧环境里,aria-hidden="true"和tabindex="-1"能覆盖其中大部分效果:辅助技术会跳过该容器,Tab 键也会跳过该输入框,只是脚本仍可让它获得焦点。 autocomplete="off"请求浏览器不要填充该字段。具体语义见 MDN 的autocomplete属性说明,并注意浏览器把它当作提示而非保证。
合起来大致是这个样子:
<div inert aria-hidden="true" style="position:absolute;left:-9999px">
<label for="contact-ref">Leave this field blank</label>
<input type="text" id="contact-ref" name="contact-ref"
tabindex="-1" autocomplete="off">
</div>
那个可见的 label 不是装饰。对于绕过了上述所有措施、仍然摸到这个字段的人来说,它是最后一道防线——所以它应该直接告诉对方该怎么做(“请勿填写此栏”),而不是解释这是个陷阱。
“在视觉上藏起来”不是检验标准。真正的标准是:使用键盘和屏幕阅读器的人能否完成表单而完全遇不到陷阱。
经典误判:自动填充
如果你提交普通表单后被拦下,原因很可能就在这一节。密码管理器和浏览器自动填充按名称、标签和类型匹配字段,并不关心字段是否可见。名为 phone 或 address2 的陷阱可能被浏览器在后台填上,服务器随即把一个真人的提交当成“机器人”提交。
浏览器扩展改写或注入表单值时也会发生同样的事。这些都不是用户做错了什么。好的实现会使用无关的字段名和 autocomplete="off" 来降低风险,并把被填上的陷阱只当作多个信号之一,而不是自动封禁。
链接与路径陷阱
同样的思路也适用于爬虫。网站添加一个真人看不到的链接(常带 rel="nofollow"),指向其 robots.txt 禁止的 URL。请求该 URL 的客户端就证明它无视了 robots.txt。
这个设计的巧妙之处,在于它不会抓到谁。你希望被收录的搜索引擎,如 Googlebot 和 Bingbot,遵守 robots.txt,从不请求被禁止的路径,所以永远不会触发。真正中招的,恰恰是本来就不遵守网站规则的客户端。
不过它也不是绝对干净。不少无害的非搜索引擎客户端——站点审计爬虫、无障碍扫描器、安全扫描器、存档机器人——会跟进它们解析到的每一个链接,而其中并非人人都会先读 robots.txt。触发陷阱只能证明某个客户端无视了规则,并不能证明它怀有恶意。这也是为什么更稳妥的做法是对相应地址限速或打标,而不是直接封禁。
计时陷阱
第三种变体甚至不需要隐藏元素。服务器在渲染表单时下发一个时间戳或签名令牌,提交时再校验。300 毫秒就返回的表单,不可能是真人读过的。但令牌必须签名或存在服务器端:直接塞在隐藏输入里的裸时间戳,只是一个客户端回传前想改就改的数字。
不过固定阈值很脆弱。自动填充能瞬间完成一份正常表单,老用户清楚该填什么,稍有耐心的机器人也只需等一等。计时最适合作为叠加在其他信号上的弱信号,绝不该成为拒绝的唯一理由。
蜜罐在整套防御中的位置
蜜罐是一道廉价的第一层过滤。它能在任何昂贵检测运行之前,抓住造成大部分表单垃圾和初级爬取的低成本脚本。对被自动化驱动的真实浏览器则无能为力,因为它会像真人一样渲染页面,脚本也可以被要求只与可见内容交互。
所以它和整条防线并存于我们的机器人检测技术指南中:自动化标志、无头浏览器泄露、行为分析,以及 Cloudflare “正在检查您的浏览器”页面这类挑战系统。每一层负责抓住下面那层更廉价的检测漏掉的东西。
如果你被拦下了
被拦下的真人几乎总是触发了自动填充或某个扩展,而不是真正的检测。可以在自己的浏览器上检查:
- 关闭密码管理器和自动填充后重试,手动输入各字段。
- 禁用会注入或改写表单值的扩展,在干净的配置文件中再试一次。
- 看看你在其他网站是否也被拦。若是,问题更可能在网络或浏览器配置,而不是某一张表单。
想知道你的浏览器向网站暴露了哪些与自动化相关的信号,可运行我们的机器人检测。它只读取浏览器本来就会报告的内容,一切都在本地运行。
常见问题
网页表单里的蜜罐是什么?
一个真人看不到也不会填写、但仍留在页面标记中的输入字段。如果它带着值回来,说明提交者读的是 HTML 而不是渲染后的页面,这是自动化的有力证据。
蜜罐能挡住所有机器人吗?
不能。它抓的是简单脚本和爬虫。被自动化驱动的真实浏览器像真人一样渲染页面,不会碰屏幕上看不见的陷阱,所以蜜罐只是多层防御中的一层。
蜜罐会误拦真人吗?
会,主要是因为自动填充和密码管理器会填隐藏字段,以及做得不好的陷阱会被辅助技术触及。谨慎的实现会使用 inert、tabindex="-1" 和 autocomplete="off",并把结果只当作一个信号。
为什么搜索引擎爬虫不会中陷阱链接?
因为它们遵守 robots.txt。陷阱链接指向被禁止的路径,守规矩的爬虫从不请求,只有无视规则的客户端才会。


