
这段时间折腾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 idcaptcha-container/div script srchttps://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.comCookie的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的DNSEmail 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作用域的问题和验证本身无关。这套机制理解透了你会发现它不是一个简单的验证码工具而是一个可以按业务场景灵活编排的安全组件用好了能让站点既安全又顺滑。