每天打开任意一个用了Cloudflare的网站,你大概率都见过那个蓝色勾选框,或者一个写着“Verify you are human”的等待页面。这就是Cloudflare人机验证最直观的样子——用户只点了一下,或者干脆什么都没点,挑战就通过了。但你知道这背后到底发生了什么吗?
人机验证这件事,早期主流方案是让用户识别扭曲文字、选红绿灯、算算术,后来又出现了Google的reCAPTCHA,让用户“点一下”表示自己不是机器人。但这些方案都有两个绕不开的毛病:一是体验差,用户得花时间配合;二是隐私争议大,因为传统验证码厂商会跟踪用户行为来区分人和机器。Cloudflare的做法不一样,它把“人机验证”从“让用户证明自己”改成了“让浏览器证明环境”,这就是我今天想认真拆一拆的东西。
这篇文章适合正在用Cloudflare、被爬虫和垃圾注册折磨的站长,也适合对前端安全、反爬机制感兴趣的人。我会讲清楚Cloudflare人机验证的原理、Turnstile的接入方式、安全配置调优,以及几个我实际踩过的坑。
1. 为什么Cloudflare要做“人机验证”这件事
1.1 机器人流量不是小打小闹
很多人对恶意机器人的认知停留在“爬虫抓我内容”这个层面,实际上的情况远比这个复杂。拿一个二线电商站来说,每天的请求里可能有40%以上不是真人,它们大致分几类:批量抓价格和库存的比价爬虫、扫接口的撞库脚本、注册垃圾账号的批量程序、刷评论刷点击量的水军。这些流量不仅消耗服务器带宽,还在污染业务数据,严重的时候能把整个站点拖垮。
Cloudflare本身是CDN和WAF服务商,每一秒都有大量流量从它的边缘节点经过,所以它在“识别请求来自人还是程序”这件事上有天然的数据优势。它的人机验证不是单纯的验证码工具,而是建立在全网威胁情报之上的一个动态门槛:同一个请求,从同一个IP发来,在凌晨三点扫登录接口,和白天正常浏览首页,得到的判定完全不同。
1.2 传统验证码的体验魔咒
传统验证码的核心思路是“让用户完成一个对机器很难、对人很容易”的任务”。早期是扭曲字母,后来是图片分类,再后来Google直接让用户勾选“我不是机器人”,实际上是通过追踪用户的点击行为、Cookie历史、浏览习惯来打地平。问题在于,真正能拦住恶意程序的验证任务往往人也很难做,体验极差,用户会在注册环节流失一大片。而为了保持体验故意放低难度的验证码,又很容易被打码平台用人工识别破解,形同虚设。
这也是Cloudflare认为“人机验证应该换个方向做”的起点。与其去设计一个更难的谜题,不如让浏览器在后台完成一系列环境检测,把结论告诉站点:这个访问者大概率是真实用户。整个过程中用户不需要做任何事,顶多点一下勾选框,或者点一下“验证”。
1.3 从“人答题”到“环境自证”的设计转向
Cloudflare对“人机验证”的最终定义,不是“你能不能答对题”,而是“你的访问环境是否可信”。一个真人用户使用的正常浏览器,和一个程序员用来跑脚本的无头浏览器,哪怕发送的HTTP请求内容一模一样,它们的底层环境也是天壤之别。
浏览器指纹、TLS握手特征、鼠标移动轨迹、CPU指令集、显卡渲染结果、系统字体列表、屏幕色深、时区、语言列表、WebGL参数,甚至音频处理产生的波形是噪声还是精确的零值……这些信息集合在一起,基本能让服务器判断出“这是不是一个人在用一个正常的浏览器”。换句话说,Cloudflare的验证机制真正的考点不是人,而是浏览器本身,人只是“浏览器的合格操作者”。
2. Cloudflare人机验证的核心技术拆解
2.1 托管挑战与JS Challenge:让浏览器替用户“答题”
Cloudflare最常见的人机验证形态是Managed Challenge(托管挑战),也叫智能挑战。触发之后,Cloudflare不会直接给用户看一堆图片网格,而是返回一个挑战页面,页面里包含一段JavaScript脚本。浏览器会执行这段脚本,完成一系列环境探测,然后把结果提交给Cloudflare边缘。全部通过后,Cloudflare会下发一个cf_clearanceCookie。
这个Cookie就是“该浏览器在这个域名下已被验证”的凭证,有效期由你在安全级别里的配置决定,短则30分钟,长则24小时甚至更久。值得注意的是,cf_clearance是绑定整个浏览器环境的,不是单纯绑定Cookie。如果你清了Cookie、换了浏览器,或者用一个带着完全不同指纹的自动化工具去访问,即使手动把Cookie复制过去,也大概率会被重新挑战。
2.2 那些你感知不到的检查项:TLS指纹、IP信誉与行为指标
写爬虫的人都知道,光伪装请求头是远远不够的。早在HTTP请求到达应用服务器之前,Cloudflare在TLS握手阶段就已经开始分析你了。
TLS指纹,专业术语叫JA3/JA4指纹。Chrome、Firefox、Safari在建立TLS连接时,ClientHello消息里的密码套件、扩展顺序、椭圆曲线参数都有各自的特征。而最常见的Python爬虫库requests,它的TLS指纹和Chrome差了十万八千里,服务器在第一个数据包阶段就能标记怀疑。这也是为什么很多爬虫即使加满随机UA、伪装Cookie,照样在Cloudflare面前一碰就死的原因。
IP信誉同样关键。Cloudflare维护着一个庞大的威胁情报库,包含过去一段时间内参与过攻击、扫描、撞库、垃圾邮件、代理池的IP地址段。如果你用的是机房IP、低信誉代理,或者访问频率忽高忽低、同一秒内并发请求过多,都会被直接判定为高风险。
行为指标则是更细的维度,比如鼠标路径是否平滑、点击间隔是否符合人类节奏、键盘输入是否有延迟波动、滚动是否自然。这些在正常用户那里是“下意识动作”,在脚本程序里则会暴露出机械、精确、缺乏加速度变化的特征。
2.3 无头浏览器与自动化工具的识别思路
有人会说,那我不请求库了,我用无头浏览器总行吧?Headless Chrome跑起来,TLS指纹和真实Chrome几乎一样,JS也能执行,看起来应该很难区分。实际上无头浏览器依然有大量破绽。
现代浏览器在正常运行时会有一些只存在于真实渲染流程里的特性,比如WebGL上下文的GPU供应商信息、Canvas渲染出的像素噪点、字体的千分位排列、浏览器扩展在DOM中注入的节点、系统级的时间源偏差。无头模式下很多特性是缺失或异常的。再加上Chromium本身在无头模式中会暴露一个HeadlessChrome的UA标记,如果被去掉,又会在navigator.webdriver属性、window.chrome对象完整性、Permissions API行为上露出马脚。
Cloudflare的检测不是单点信任,而是综合打分。只要有一两个特征异常,就会进入挑战流程;如果多项异常且请求路径敏感,比如登录、注册、提交表单,就会直接被拦截。这套机制的可怕之处在于,它是动态迭代的,Cloudflare每隔几周就会悄悄加入新的检测维度,写反爬工具的团队只能被动追踪,很难做到长期稳定的绕过。
2.4 Turnstile:不依赖跨站追踪的独立人机验证
2022年Cloudflare发布了Turnstile,这是一个可以嵌入任意网站的独立人机验证产品,类似于reCAPTCHA的全新替代品。最大的区别是它不像Google那样依赖跨站点Cookie和长期追踪来评估用户,而是基于当前页面里的挑战环境实时判断,更贴合隐私法规的要求。
Turnstile有几种渲染模式:隐形模式全程无感,后台自动完成验证;受管模式会显示一个挑战框,但大多数情况下用户不需要点击,它自动通过;非侵入式模式则要求用户点一下勾选框。对站长来说,Turnstile最爽的一点是它可以脱离Cloudflare CDN单独使用,也就是说你的服务器可以放在任何地方,网站也能接入这套人机验证。而且它不要求深度集成,前后端加起来几十行代码就能跑通。
3. 实操:给你的网站接入Turnstile人机验证
3.1 创建站点并获取Site Key与Secret Key
接入Turnstile的第一步是登录Cloudflare控制台,在左侧菜单找到Turnstile入口。点击“Add Site”之后,需要填写站点名称和域名,然后选择验证模式。官方给了三种:Managed(受管)、Non-Interactive(隐形)、Invisible(不可见)。我通常建议首选Managed,因为它在用户完全不操作时自动通过,只有在风险较高时才展示交互挑战,平衡体验与安全性。
提交后你会得到两串关键信息,这就是你的钥匙了。
| 参数 | 用途 | 安全等级 |
|---|---|---|
| Site Key | 嵌入前端页面,公开可见 | 低 |
| Secret Key | 后端服务器校验Token,严禁暴露 | 高 |
这里有一条新手最容易犯的错误:把Secret Key写在JavaScript里,等于把你的验证后门大敞四开。任何看到网页源码的人都能用你的密钥去伪造校验结果。Secret Key只允许放在服务器端,绝对不要出现在前端代码或Git仓库里。
3.2 前端接入:显式渲染与隐式渲染
Turnstile的前端接入有两种方式:一种是用官方脚本加一个div标签,让脚本自动渲染;另一种是显式调用JavaScript API来控制验证框的位置和交互逻辑。
先看最简洁的自动渲染方式:
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <div class="cf-turnstile"><script> window.onloadTurnstileCallback = function () { turnstile.render(document.getElementById('captcha-box'), { sitekey: 'YOUR_SITE_KEY', callback: function (token) { document.getElementById('turnstile-token').value = token; }, 'expired-callback': function () { // Token过期时需要让用户重新验证 document.getElementById('turnstile-token').value = ''; }, }); }; </script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" async defer></script> <input type="hidden" id="turnstile-token" name="turnstile-token" value="">我建议你在生产环境里一定要监听expired-callback。Cloudflare下发的验证令牌有效期只有2分钟,如果用户填表填到一半超时了,再提交表单就会后端校验失败。监听这个回调,把隐藏域清空,并且在提交时检查Token是否为空,能避免大量莫名其妙的报错。
3.3 后端校验:siteverify接口与返回参数解读
前端获得的Token本身不代表验证通过,它只是一张“待兑奖券”,真正的判定必须由后端调用Cloudflare接口完成。也就是说,黑客可以绕过前端直接POST,但拿不到合法的Token,后端校验这关就过不去。
后端请求地址是固定的:
curl -X POST 'https://challenges.cloudflare.com/turnstile/v0/siteverify' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data 'secret=YOUR_SECRET_KEY&response=用户提交的TOKEN&remoteip=用户访问IP'返回结果是一个JSON对象,正常情况下是这样的:
{ "success": true, "challenge_ts": "2024-06-12T10:30:00.000Z", "hostname": "example.com", "error-codes": [] }有几个点需要提醒你。返回值里有hostname字段,那是生成Token的站点域名,后端一定要校验它和你站点的域名一致,防止攻击者拿到别人站点的合法Token来提交你的表单。remoteip参数建议传,这样Cloudflare可以校验Token和IP是否绑定,但注意如果用户的IP是动态变化的,比如移动网络,你需要在生成页面时存好一个会话里的IP,保证前后端判定一致。
常见的error-codes我记得有这么几种:missing-input-secret表示未传Secret Key,invalid-input-secret表示Secret Key格式错误,missing-input-response表示没收到前端Token,invalid-input-response表示Token不存在、已过期或已被使用过,bad-json表示请求体不是合法的JSON格式。排查问题的时候先对照这个表判断阶段,比瞎猜快得多。
3.4 进阶防护:结合Token时效、Double-Submit Cookie与重复校验
很多人以为加了Turnstile就万事大吉了,其实不是。Token只能证明“这个浏览器在你接入了Turnstile的页面上通过了验证”,不能证明“这次提交表单的这个会话没有被劫持”。建议把Token校验和会话机制配合起来。
一个简单但有效的组合方案是Double-Submit Cookie:登录或进入表单页时,后端生成一个随机值写入HttpOnly Cookie;前端提交表单时把这个随机值放在隐藏字段里;后端校验隐藏字段的值和Cookie里的值是否一致。再加上Turnstile Token,就形成了“密码学Token + 会话绑定 + 前端环境验证”三道防线。对于普通业务站点,做到这个程度已经能挡住绝大多数恶意提交了。
另外一个容易漏的点是,Turnstile Token是一次性的,用过后立即失效。也就是说,你的后端接口必须保证Token只被校验一次,并且校验完成后要把Token记录到临时存储或加个缓存标记。如果不做这步,攻击者可以在自己页面上先获取一个合法Token,然后批量重放你的提交接口,照样能把你的服务打崩。
4. 在Cloudflare站点上配置验证策略
4.1 Security Level 与 Under Attack Mode 的联动
接入Cloudflare CDN之后,人机验证不一定要靠Turnstile单独做,Cloudflare本身就能在边缘层替你拦截恶意流量。Security Level(安全级别)是最基础的设置,它决定了一个访客在触发什么条件时会被要求完成托管挑战。
五个档位的逻辑是这样的:基本上所有人都信任的IP,比如搜索引擎爬虫、企业IP段,在Even with low风险下也不挑战;而对于机房和高风险IP,在Medium档就会触发挑战。我实际配置的经验是:有论坛、评论区、注册页的站点建议调增强档,也就是在需要时对可疑IP直接加验证;如果站点同时开了Turnstile做业务层验证,边缘层的安全级别可以适当放宽,避免同一用户被验证两次,降低体验损失。
Under Attack Mode(攻击模式)则是另一个极端。开启后Cloudflare会给所有访客显示一个等待挑战页面,做5秒的JavaScript计算,通过后再进入站点。这个模式只建议在站点正遭受大规模攻击时临时开启,因为连搜索引擎爬虫也会被拦,对自然流量伤害非常大。我见过有人把攻击模式当成常驻设置,结果站点搜索收录量掉了一半,得不偿失。
4.2 Bot Fight Mode 与托管挑战怎么搭配
Bot Fight Mode是Cloudflare提供的另一层机器人防护,在Dashboard的Bots菜单里开启。免费版和Pro版提供的是简化版逻辑,主要针对请求特征明显的非浏览器流量,比如缺UA的脚本、请求频率异常的IP。Bot Fight Mode和管理挑战机制是不冲突的,Bot Fight Mode负责“击落”,托管挑战负责“筛选”。
合理的搭配策略是:对前端页面路径,比如首页、文章页,开启Bot Fight Mode,把明显的机器人流量直接拦截;对登录、注册、提交表单的业务路径,配置WAF自定义规则,用Managed Challenge而不是直接拦截,因为误杀真人的损失远大于放过一两个机器人。Cloudflare的WAF自定义规则支持Managed Challenge动作,你可以非常灵活地指定哪些Path、哪些国家、哪些IP段需要挑战。
4.3 防止误伤真实用户的几条调优原则
人机验证最大的坑不是拦不住机器人,而是把真人挡在门外。我在调优过程中总结出几条原则,基本能避开绝大多数误伤。
第一,给真正的爬虫开会后门不可取,但给搜索引擎放行是必须的。Googlebot、Bingbot这些搜索引擎爬虫有公开的DNS反向解析和IP段,Cloudflare默认会识别,但最好再写一条WAF规则明确放行,否则站点内容收录会出问题。
第二,对低频访问的新用户,不要一上来就用高强度验证。把安全级别设为Medium,优先挑战高风险IP,而不是挑战所有IP。很多真实用户只是换了个网络环境,比如刚从办公楼切换到公共WiFi,IP信誉不高但行为正常,又正好赶上了高安全级别的站点,就会被无端验证。
第三,记录防火墙事件日志。Cloudflare的Security Events面板能看到每个被拦截或挑战的请求的详细特征,包括IP、ASN、路径、是哪个规则触发的。定期看这个日志,把被频繁误伤的IP段加入白名单,或者把产生误伤的规则改得更精准,是长期维持良好体验的唯一办法。
5. 常见问题排查与避坑实录
5.1 页面一直转圈、验证总是不通过的排查顺序
这是被问得最多的问题,用户反馈“我明明是真人在操作,验证框却一直转圈,或者点击验证后没反应”。这种问题我建议按下面的顺序排查。
首先看浏览器系统时间是否正确。Cloudflare的盾牌验证依赖时间戳计算,如果本机时间比服务器慢几分钟,挑战脚本生成的凭证就是无效的。这个问题在重装系统、电脑休眠过久、主板电池没电的机器上最常见。其次看Cookie状态,挑战成功需要写入cf_clearanceCookie,如果浏览器禁用了第三方Cookie或者开了严格隐私模式,验证很难完成。然后是浏览器扩展,广告拦截类和隐私保护类扩展经常会把挑战脚本的请求拦掉或改掉,导致验证死循环。实测下来,开启“严格模式”的uBlock和某些翻译插件都有这种问题。
如果你做的是Turnstile接入,一直转圈还要额外检查是否引入了多个版本的api.js脚本,或者重复调用了turnstile.render。同一页面只能有一个Turnstile实例正常工作,重复渲染会导致第二个验证框永远等不到回调。
5.2 真实用户被误拦怎么办
人机验证误拦真人有两类场景:一类是站点安全级别设置得太激进,另一类是用户的网络环境被Cloudflare标记为高风险。
如果是第一类,处理方式很简单:在Security Level里把档位降一档,或者在WAF规则里针对被误伤的路径放行。如果是第二类,就麻烦一点,因为问题出在用户侧,不是你的配置。常见的用户侧原因有:用户用的是机房IP或共享出口IP,这个IP之前被用作攻击源或爬虫池;用户的网络网关设备被植入恶意插件,产生了大量可疑流量;用户的本地DNS被污染,导致请求头里出现异常。
这时候你可以在Cloudflare的Security Events里把该用户的IP拉出来看,如果确认是共享IP导致的误判,可以添加一条Country或ASN级别的信任规则,把这家运营商的相关IP段放行。但要注意不要放得太宽,否则机器人也会钻空子。
5.3 “找不到电子邮件路由”报错排查
这个报错是我最近帮一个朋友排查时遇到的,他在Cloudflare控制台想给自己的域名启用Email Routing,结果后台提示“找不到电子邮件路由”,同时他在网站后台怎么也收不到管理员验证邮件。乍一看和人机验证没关系,但把链路追下来,你会发现它们经常卡在同一个环节。
Cloudflare Email Routing是一个邮件转发服务,它需要你确认域名所有权并创建相应的DNS记录才能激活。激活的标准动作是:在Email Routing页面里点击“Start setup”,然后按要求在DNS页面添加MX记录和TXT记录。如果没有正确添加,或者添加后没有完成验证,控制台就会显示找不到电子邮件路由。
排查时首先看DNS记录,确认域下的MX记录指向的是Cloudflare的route1.mx.cloudflare.net等值,TXT记录里含有v=spf1 include:_spf.mx.cloudflare.net之类的校验字符串。其次看域名控制权,Cloudflare的邮件路由验证要求你的域名必须把DNS托管在Cloudflare上,也就是说必须使用它的NameServer。如果域名换过DNS服务商,或者中途调整过NS记录,这个报错就会出现。
还有一个隐藏原因,很多人在第一步收到“We sent you a verification email”之后就干等着,没注意到验证邮件被过滤了。因为Cloudflare的邮件验证是从verify@cloudflare.com发出的,部分企业邮箱把它们当成垃圾邮件扔进回收站,你自己那里没看到,后台就一直卡在“未验证”状态,表现出来的就是找不到路由。
处理的完整顺序是:检查DNS记录是否完整、确认域名NS是否在Cloudflare、去垃圾邮件里翻验证邮件、如果实在没有就点“Resend verification email”重新发送,收到点击确认后,路由才会被激活,这时候再去网易邮箱配置转发规则,接收管理员邮件。
5.4 我的几点经验随笔
接触Cloudflare人机验证这几年,我最深的体会是:没有一套验证方案是百分之百完美的,所谓的安全,本质上是在“拦住机器人”和“不惹恼真人”之间找平衡。
一个很现实的例子是,Turnstile接入初期我为了追求“绝对安全”,把校验逻辑写得特别死,Token过期时间、IP绑定、Double-Submit Cookie全开了,结果线上马上出现一批老用户投诉,说表单提交老失败。后来查下来,大部分失败发生在用户填写时间超过2分钟、Token过期但页面没有自动刷新验证的场景。后来我把expired-callback的刷新逻辑补全,并且在Token过期时自动调用turnstile.reset(),投诉量立刻降为零。细节这种东西,真的是要用线上事故来换。
还有一点想多说一句,Cloudflare的挑战机制和互联网的攻防博弈一样,是动态演进的。今天管用的检测维度,明天可能就被绕过;今天好用的拦截策略,过两个月可能误伤一片人。所以别把安全配置调完就彻底不管了,定期去Security Events面板翻一翻,看看最近被人机验证挡住的请求里有多少是真的可疑流量、有多少是误伤。数据会告诉你,你的配置到底是不是合理。
如果你正在给自己的站点接人机验证,建议先开Turnstile试水,再逐步调整Cloudflare边缘层的挑战策略。这个组合是目前我试过的里面对真实用户最友好、对机器人最不客气的一套方案。