这段时间折腾Cloudflare人机验证,前后把它从“能用”调到了“顺滑”,顺便把坑也都踩了一遍。如果你做过站点防护,应该对那个“验证您不是机器人”的页面不陌生,这就是Cloudflare的托管挑战,背后是一整套叫Turnstile的人机验证体系。很多人以为它只是个“换皮验证码”,其实不是,它本质上是一个集流量信誉、浏览器指纹、行为分析和规则引擎于一体的自动化防御系统。这篇文章我把Cloudflare人机验证的底层逻辑、接入方式、规则调优和报错排查一次讲透,适合站长、运维、前端开发和对站点安全感兴趣的朋友参考。
我在实际项目中遇到过的情况是:有些站点开了Cloudflare之后,真实用户被验证页面卡住,流失率肉眼可见地涨;另一边,攻击者的爬虫却还在乐呵呵地刷接口。问题不出在Cloudflare不行,而是你对“人机验证”的认知还停留在“验证码”这个层面。Cloudflare的验证体系不是一道锁,它更像一个门卫,会根据访客的长相、行为、来的路线,决定是直接放行、问一句,还是直接拒之门外。理解这一点,后面的配置和调优就顺理成章了。
1. 先搞懂Cloudflare人机验证到底在验证什么
1.1 为什么网站要区分人和机器
互联网上每天有大量请求不是真人发出来的。搜索引擎爬虫是善意的,但更多的是恶意的:扫描器在猜你后台路径,撞库脚本在试登录接口,内容采集器在批量扒你的文章,攻击者用分布式代理刷你的API耗尽资源。这些请求单看每一个都和正常访问没什么两样,但如果没人拦,服务器迟早被拖垮。
传统验证码的做法是出题考人,比如扭曲的数字、图片里的红绿灯,通过“人类擅长而机器不擅长”的任务来分流。但Captcha类方案已经有十几年历史,深度学习成熟之后,文字识别、图片分类对机器来说早就不难了,反而是真人用户每次都要辨认半天,体验极差。Cloudflare换了个思路:不出一张卷子,而是看“你这人是怎么走进门的”。
人机验证在这里的真正价值,是不打扰地识别人和机器。一个真实访客的浏览器有完整的历史、插件、字体、Canvas渲染特征,鼠标在页面上有自然的加速度和停顿,请求链路也符合普通宽带用户的特征。而一台无头浏览器或脚本,哪怕伪造了User-Agent,在指纹层面还是会漏出破绽。Cloudflare把这些信号综合起来打一个分,分数够了就直接放行,不够才弹出挑战。
所以你在Cloudflare后台看到的“Security Level”“Managed Challenge”“Turnstile”这些概念,本质都是在说一件事:什么样的请求值得怀疑,怀疑到什么程度弹出验证,验证通过之后能管多久。理解了这个框架,后面调参数就不会瞎试了。
1.2 一套验证体系的三层“挑战姿势”
Cloudflare人机验证不是单一方案,而是分了三层,按威胁等级和业务场景选用。第一层是JS Challenge,也就是纯JavaScript挑战。它给你的浏览器一段脚本,要求计算出一个结果,通过后种一个cf_clearanceCookie,全程没有弹窗,用户无感。它主要用于拦截那些连JavaScript都不执行的请求——比如最简陋的爬虫和扫描器,因为正常浏览器一定会执行JS。
第二层是Managed Challenge,也就是我们最常见的“验证您是不是机器人”页面。这一层会综合IP信誉、客户端指纹、TLS指纹和浏览器环境,对高风险请求弹出托管挑战。托管挑战不一定每次都让你点选图片,很多情况下你在页面停留一两秒就自动通过了,这个“自动通过”其实就是后台已经根据你浏览器环境的得分提前做了判断。只有得分卡在灰色地带的请求,才会看到真正可交互的验证码控件。
第三层是Turnstile,这是Cloudflare独立出来的人机验证组件,不要求你把整个站点接入Cloudflare CDN也能用。Turnstile有托管模式、非交互模式和隐形模式三种呈现方式,你可以把它嵌入登录框、注册页、评论提交和任何自定义表单。它和Cloudflare既有的安全体系共享同一套风险信号,所以同一个设备在别处被标记过,转到你站点上也能识别出来。三层配合,构成了从“完全无感”到“显式交互”的完整梯度。
1.3 无感通过背后的几类关键信号
Cloudflare到底看了你什么信息?公开披露和社区逆向整理下来,主要信号集中在几类。第一类是TLS和HTTP指纹,客户端建立HTTPS连接时,ClientHello里的密码套件顺序、扩展列表、椭圆曲线参数,组合起来等于浏览器的一张身份证,一个伪装成Chrome的Python脚本,TLS指纹立刻暴露。第二类是浏览器运行时指纹,包括Canvas渲染结果、WebGL参数、字体列表、屏幕分辨率、时区,这些组合在一起,独一无二到不亚于指纹。
第三类是行为数据,鼠标轨迹、键盘延迟、滚动节奏、页面聚焦切换,真人操作充满随机噪声,而自动化脚本要么没有,要么过于机械。第四类是网络与信誉数据,请求IP的ASN归属、历史攻击记录、数据中心IP段、代理出口特征,Cloudflare每天都在更新这些威胁情报库。第五类是浏览器存储状态,比如LocalStorage、IndexedDB中由Cloudflare种下的历史标记,你的设备如果之前在其他站点上被判定为可疑,这个记录会跟着你走。
理解了这些信号,你就能明白两个衍生结论。第一个,为什么开了严格安全等级后老用户也会被弹验证?因为他们换网络、换浏览器后,原来的设备指纹对不上了,系统重新进入低置信区间。第二个,为什么单纯的“输入正确验证码”还不够?因为验证码本来就是最后一道兜底,前期的风险信号已经决定了你大概率要过这关。策略上,Cloudflare更希望那些“本身就没问题的请求”直接放行,而不是让每个人都做一遍题。
2. Turnstile接入:从控制台新建到前后端联调
2.1 创建密钥时最容易踩的两个坑
Turnstile的设计很友好,可以脱离Cloudflare的CDN单独使用,所以很多业务方拿它来自建表单防护。第一步是登录Cloudflare控制台,在左侧找到Turnstile入口,点击“Add Site”。需要注意的是,这里的“Site”不等于“域名”,而是指一个使用场景,比如“登录表单”“评论提交”“注册接口”,你可以一个域名建多个Site,按照业务用途分开管理。
创建时会让你填域名,支持通配符子域名,比如example.com和*.example.com。创建成功后会得到一对密钥:Site Key是公开的,放在前端页面里,用于渲染验证组件;Secret Key是私密的,只能存放在服务端,用于向Cloudflare验证token。我见过不少人在前端代码里写死Secret Key,然后被扫到后疯狂刷接口,等于把钥匙挂在门上还告诉别人这把钥匙能开门。
配置完立即能看到测试用的虚拟密钥,但要注意:虚拟密钥只能用于测试,流量一上去就会在面板里看到大量验证失败。第二个坑是本地开发域名,如果你的本地环境是localhost或127.0.0.1,创建Site时必须把localhost也作为主机名加上,否则本地调试会一直转圈。我习惯在控制台把localhost和一个测试子域名同时配上,生产、预发、本地三套环境各用各的Site Key,避免互相踩。
2.2 前端接入代码拆解
Turnstile前端接入非常轻。在需要显示验证组件的HTML里放一个<div>容器,然后在页面加载时动态引入官方脚本,调用turnstile.render()把组件渲染到容器里。以下是一个最基础的托管模式例子:
<div id="captcha-container"></div> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script> window.onload = function () { turnstile.render('#captcha-container', { sitekey: '你的_Site_Key', callback: function(token) { document.getElementById('captcha-token').value = token; }, 'error-callback': function() { console.error('验证组件加载失败'); } }); }; </script>这里有几个容易被忽略的细节。callback是验证成功后的回调,Cloudflare会返回一个token字符串,你需要把它放进表单隐藏域,等用户提交表单时一起发给后端。如果用户验证成功后刷新了页面,token就会失效,所以不要在页面初始化时提前验证并存储token,最好在表单提交前确认当前token仍有效,失效就重新调用turnstile.reset()再渲染一次。
Turnstile支持三种模式。托管模式(Managed)下,Cloudflare根据风险决定显示“非交互式通过”还是“交互式挑战”,对用户最友好;非交互模式(Non-interactive)适合要求低干扰的场景,一般几秒内自动通过;隐形模式(Invisible)则完全不显示组件,仅凭后台信号判断。三种模式的接入代码差别很小,只是render()时的参数不同,但隐形模式对流量信号要求高,如果站点本来就没什么信誉数据,直接上隐形模式很可能误杀率高,起步阶段建议先托管模式。
2.3 服务端校验必须做,且不能只做一半
前端拿到token后,服务端必须向Cloudflare的siteverify接口发起请求,确认这个token有效。为什么必须做?因为前端拿到的一切都可能是伪造的,恶意用户完全可以绕过页面直接构造请求,跳过前端验证。token只是“前端验证通过”的凭证,服务端不校验等于没验证。
后端校验接口的标准方式是发起POST请求:地址是https://challenges.cloudflare.com/turnstile/v0/siteverify,请求体带上secret(你的Secret Key)、response(前端传来的token)、以及可选的remoteip(用户IP)。Cloudflare返回的JSON里有一个success字段,为true才算通过。同时注意,token是一次性的,而且有效期大概300秒,过了时间再用就会返回失败。这也意味着,一个页面设计成“提前验证、最终过很久才提交”是会有问题的,比如用户填表花了10分钟,token早已过期,提交时后端必然校验失败。
如果你用Node.js,可以这样封装一下校验逻辑:
async function verifyTurnstile(token, ip) { const form = new URLSearchParams(); form.append('secret', process.env.TURNSTILE_SECRET_KEY); form.append('response', token); if (ip) form.append('remoteip', ip); const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: form }); const data = await res.json(); return data.success === true; }处理返回值时,除了success字段,最好把error-codes也记录下来。比如invalid-input-response表示token格式不对或已被使用,timeout-or-duplicate表示token过期或重复提交。把这些错误码存到日志里,后面排查问题会省很多时间。
2.4 本地开发调试的建议
本地调试Turnstile最烦的一点是,默认情况下localhost可能不在允许的主机名列表里,组件会渲染不出来。解决方案前面说过,在控制台添加localhost主机名。但即便加了,本地hosts把域名指向127.0.0.1的场景也常出问题——如果你绑定了dev.example.com到本地,控制台对应的Turnstile Site里主机名要有dev.example.com。
调试阶段我通常还会临时把安全级别调低,避免明明是人却被挑战。另外,浏览器里开了严格隐私模式或者装了去广告插件,也可能拦截challenges.cloudflare.com的脚本,导致组件一直不加载。排查时先开无痕窗口,把插件全停掉,看组件是否出现;如果出现了,说明是你本机环境拦截了脚本,而不是配置问题。
还有一点:Turnstile验证组件在移动端WebView里表现不太稳定,如果你有App内嵌页,建议在WebView中开启JavaScript并允许第三方Cookie,否则验证状态很可能会丢失。这块在项目上线前一定要真机测一遍,模拟器很多行为测不出来。
3. 用安全级别和规则引擎把人机验证变成业务策略
3.1 安全等级到底在调什么
Cloudflare安全等级(Security Level)并不是“越严格越安全”,它的本质是“对可疑请求弹出挑战的阈值”。后台里安全等级从Essentially Off到I’m Under Attack分五档,每一档对应一个威胁分阈值。威胁分由访客IP信誉、UA异常程度、浏览器指纹可信度等综合计算,分数越高代表越可疑。
当访客的威胁分超过你设定的阈值,Cloudflare就会返回挑战页。对普通静态内容站点,我建议保持Medium档,这个档位下正常访客几乎不会被挑战,只有明显异常的请求才需要过验证。如果站点经常被CC攻击,临时调到High甚至I’m Under Attack能显著降低源站压力,但代价是部分企业代理、校园网出口、IPv6大流量出口都可能被误伤,因为这些IP段的信誉分天然不高。
这里要特别提醒:I’m Under Attack不仅对所有可疑请求加挑战,还会对页面里的所有资源请求都做JS挑战,如果站点页面加载了上百个静态资源,每个资源都先过一轮JS挑战,页面会明显变慢。这个模式只建议在攻击进行时临时开,不要当默认策略。我在一次客户事故里看到他们把安全级别常年设在I’m Under Attack,结果搜索引擎的爬虫都被卡在外面,收录量直接腰斩。
3.2 自定义规则:只对关键接口启用验证
很多人不知道Cloudflare人机验证可以做成“策略化”,而不是全站一刀切。通过WAF自定义规则,你可以精确控制哪些请求需要验证,哪些请求直接放行。比如,你的站点只有一个登录接口怕撞库,那就没必要让所有游客都过验证,完全可以只在登录请求上启用Managed Challenge。
配置路径是:Cloudflare控制台 → Security → WAF → Custom Rules,新建规则时,条件选“URI Path”等于/api/login,动作选“Managed Challenge”或“Interactive Challenge”。这样普通浏览站点的用户毫无感知,只有在提交登录表单时才会触发验证,体验和安全性都兼顾了。
类似的策略还可以这么做:对后台路径/wp-admin强制挑战;对API接口/api/v1/返回403而不做挑战,因为API本来就该用Token鉴权,不是人机验证的适用场景;对静态资源目录直接Skip所有安全规则,用缓存扛流量,反正没有敏感逻辑。把这些规则按优先级排好,前面命中就后面的规则不再执行。我习惯把放行规则放最前面,比如“已知可信的客户端ASN放行”“站点自己的监控探针放行”,然后是挑战类规则,最后才是默认动作。
3.3 Challenge Passage和cf_clearance的“会话时限”经验
验证通过后,Cloudflare会给浏览器种一个名为cf_clearance的Cookie,这个Cookie的存在时间由Challenge Passage决定,默认是30分钟。在Cookie有效期内,同一浏览器再次访问同一站点不会再次被挑战,这就是用户“验证一次,管一段时间”的原理。
这个“放行时长”怎么设很有讲究。设太短,比如5分钟,用户访问几个页面后切回来又要验证,流失率高;设太长,比如24小时,如果这是一个共享电脑、公共WiFi环境,之前的可疑身份会让后面的正常用户也被放行,存在一定风险。操作路径在Security → Settings → Challenge Passage,我一般建议普通内容站点设30分钟,涉及支付、账号操作的站点设5到10分钟,让敏感操作保持较高频率的验证。
还有一类体验问题:用户明明验证通过了,但跳转后又被弹出来一次。这多半是因为页面里加载了外部静态资源,比如图片走了另一个CDN域名,或者页面发生了多次302跳转,导致cf_clearance在中间环节没种上。排查方法很简单,开发者工具看Application面板里的Cookie,确认cf_clearance是否存在,如果存在但仍然被挑战,就要检查是不是有代码在某个环节清理了Cookie。
4. 高频报错与排查实录:从转圈到邮件路由
4.1 验证框一直转圈
这是接入Turnstile时遇到最多的问题。验证框一直转圈,说明组件根本没有拿到有效响应。第一件事是按F12看Console和Network,确认challenges.cloudflare.com/turnstile/v0/api.js有没有加载成功。如果这个脚本返回403或者直接被浏览器拦截,多半是广告拦截插件、隐私扩展或企业安全软件把它当成了跟踪器。Turnstile官方对这个情况有一个降级机制,会让组件最终显示一个可点击的验证框,而不是彻底卡死。
如果脚本加载正常但组件还是转圈,检查你的容器<div>是否设置了display:none或宽度为0。Turnstile组件需要可见区域来完成渲染,在隐藏容器里渲染会有问题,解决办法是等容器可见后再调turnstile.render(),或者用turnstile.execute()这种方式在需要时才真正执行验证。
还有一个经常被忽略的因素是页面里同时引入了多个不同版本的Turnstile脚本,后加载的脚本把前一个的全局实例覆盖了,导致两个组件互相干扰。用官方CDN时就不要自己再本地化部署一份API脚本,如果实在要自托管,全站保持同一个版本。出现这类问题,最快捷的排查方法是把页面改成只保留一个最小化测试用例,能复现问题再逐步加回原来的功能。
4.2 siteverify返回invalid-input-response
服务端校验时遇到invalid-input-response,大多数情况下不是Cloudflare的问题,而是你自己的代码把token弄丢了或者弄错了。这个错误字面意思是“你提交的response不是有效的输入”。我从项目里总结出三个最普遍的原因:
token本身为空。前端没把token塞进表单隐藏域,或者塞进去的字段名在服务端读错了。前后端字段名不一致是典型问题,比如前端写captcha_token,后端读turnstile_token,结果读到undefined,传过去当然校验失败。
token被重复使用。Turnstile token是一次性的,前端如果因为点击了多次提交按钮,同一个token被拿去校验了两遍,第二次必然返回这个错误。如果你的登录按钮没有做防重复提交,用户连点两下,第一下成功了,第二下就会看到校验失败。
token已经过期。前面说过有效期大约300秒,如果用户打开页面后停留超过5分钟才提交表单,这个token已经不能用了。这种情况更好的交互方式是提交前通过JavaScript调用turnstile.execute()获取一个最新token,或者在提交接口返回timeout-or-duplicate时,提示用户刷新验证组件再试。
排查时把请求参数和Cloudflare返回的完整JSON都打日志,尤其是error-codes字段,它能精确区分是格式错误还是超时重复,比你自己猜高效太多。
4.3 明明验证通过了,还是被拦截
这个问题的症状是:用户在Cloudflare的验证页上成功完成了人机验证,浏览器地址栏也正常跳转到了目标页面,但请求还是被拦截,或者页面里的某些接口返回403。核心原因多数是cf_clearanceCookie没有在整个请求链路里生效。
举个例子,页面跳转时从http://example.com跳到了https://example.com,Cookie的Domain或Secure属性如果不匹配,浏览器就不会发送cf_clearance。再比如,页面里某个Ajax接口请求的是api.example.com,和当前页面的www.example.com不同域,跨域请求默认不带Cookie,自然会被Cloudflare判定为未验证身份。解决办法是在前端请求里显式带上credentials: 'include',并且服务端接口配置CORS时把Access-Control-Allow-Credentials设为true。
另一个隐蔽场景是自定义规则之间的优先级冲突。比如你写了一条“所有来自海外的请求都拦截”的规则,又写了“登录接口做Managed Challenge”的规则,访客属于海外IP,那请求在走到登录接口前就被拦截了,根本轮不到人机验证。我在排查中习惯先在WAF的Events页面看请求命中了哪条规则,规则动作、规则名称、命中时间都清清楚楚,比自己盲猜高效得多。
4.4 附带排查:为什么在Cloudflare后台找不到电子邮件路由
很多人在配完域名后,想用Cloudflare的Email Routing做邮件转发,结果在控制台里翻了一圈找不到入口,开始怀疑是不是自己账号没有这个功能。其实Email Routing的入口不在首页侧边栏,而是在你进入某一个域名之后,左侧菜单里的“Email”模块下。如果你连“Email”菜单都看不到,大概率是当前域名的套餐或区域状态有问题——比如域名还处于Pending状态,或者域名没有完全接入Cloudflare的DNS,Email Routing需要DNS记录能由Cloudflare托管才能生效。
还有一个非常典型的坑:你之前已经在其他服务商那里设置了MX记录,Cloudflare检测到你的域名MX记录指向外部服务器,就会在Email Routing页面提示“Cannot enable Email Routing”,并且不给你打开开关。这时需要先到DNS管理页面,把现有的MX记录删掉或改为由Cloudflare接管,然后重新尝试启用。启用成功后,Cloudflare会自动创建必要的MX记录和TXT验证记录,你只要再添加自定义地址和转发目标就行。
顺带提醒一句:如果域名启用了Email Routing,而你之后把域名的DNS托管从Cloudflare迁走了,邮件转发就会一并失效,因为那些MX记录是Cloudflare自动管理的,不会跟着你走。换句话说,Email Routing依赖Cloudflare DNS,这是很多人“邮件突然收不到”的根源。
结尾
我实际项目里踩得最多的坑,不是验证本身配不对,而是没分清“全站挑战”和“关键接口挑战”的区别,一个安全等级调太高,把真实用户全挡在外面。后来我把防护策略拆成了三块:静态资源靠缓存,核心业务接口靠Turnstile,后台管理路径靠Managed Challenge,效果比之前好很多。如果你刚上手Cloudflare人机验证,建议先从托管模式开始,不要一上来就开隐形模式,先观察一段时间后台的挑战率和拦截日志,确认误杀率在可接受范围,再逐步收紧策略。另外,本地调试时把控制台所有相关日志打开,把Cloudflare返回的每一个错误码都查明白,很多问题其实都是参数传递和Cookie作用域的问题,和验证本身无关。这套机制理解透了,你会发现它不是一个简单的验证码工具,而是一个可以按业务场景灵活编排的安全组件,用好了能让站点既安全又顺滑。