验证码基础与前端嵌入
表单防刷、注册防爬、登录防爆破,这些场景过去基本都用 reCAPTCHA。但 reCAPTCHA 体验越来越差,又要选红绿灯又要拼图,移动端还经常卡死。Cloudflare Turnstile 是 Cloudflare 推出的无感验证码服务,目标是替代 reCAPTCHA,绝大多数情况下用户什么都不用做就通过验证。这篇先把 Turnstile 的核心概念和前端嵌入讲清楚,后端校验留到下一篇。
Turnstile 与 reCAPTCHA 的区别
Turnstile 不依赖用户点击或拼图,而是通过浏览器环境信号判断访客是不是真人。两者对比
| 对比项 | reCAPTCHA | Turnstile |
|---|---|---|
| 验证方式 | 选图、拼图、勾选复选框 | 多数情况无感,少数才弹交互挑战 |
| 用户操作 | 经常需要点击 | 多数零点击 |
| 隐私 | 依赖 Google 行为画像 | 不依赖第三方画像,数据不留存 |
| 品牌 | Cloudflare | |
| 免费额度 | 有量级限制 | 无限免费 |
| 国内可用性 | 经常加载失败 | 通过 Cloudflare 边缘节点,更稳定 |
| 是否要 Google 账号 | 要 | 不要 |
最直观的感受是 Turnstile 加载快、不卡用户。后台逻辑是它收集浏览器指纹、JS 挑战、TLS 指纹等一系列信号做综合判断,可信访客直接放行,可疑的才需要交互。
站点密钥与密钥
每个 Turnstile 站点会生成一对密钥
| 密钥类型 | 英文名 | 用途 | 是否公开 |
|---|---|---|---|
| 站点密钥 | sitekey | 嵌入前端 HTML,标识是哪个站点 | 公开,可写在前端 |
| 密钥 | secret key | 后端校验 token 时携带 | 严禁泄露 |
站点密钥写在前端没问题,它本身不能验证任何东西,只是告诉 Turnstile 这次的挑战属于哪个站点。密钥才是真正用来校验的凭证,一旦泄露别人就能伪造校验结果,必须放在服务端。
在 Cloudflare 控制台左侧找到 Turnstile,点击添加站点,填入域名后会立即拿到这对密钥。本地开发时把 localhost 加进域名列表,否则本地测试会报域名不匹配。
Widget 的两种渲染方式
Widget 就是页面上那个验证码组件。Turnstile 提供两种渲染方式
| 渲染方式 | 说明 | 适用场景 |
|---|---|---|
| 显式渲染 | 用 JS 调用 turnstile.render 手动挂载到指定节点 | 单页应用、动态加载表单 |
| 隐式渲染 | 在 div 上加 cf-turnstile 类,脚本加载后自动渲染 | 传统多页站点、静态表单 |
两种方式的内部行为一致,区别只在挂载时机。隐式渲染简单粗暴,加个类名就行,但页面切走再切回来时组件不会自动重渲染。显式渲染更灵活,适合在 React、Vue 这类框架里精确控制生命周期。
三种渲染模式
更关键的概念是 widget 模式,它决定用户看到什么。通过站点配置或 data-attribute 指定
| 模式 | 英文名 | 用户感知 | 适用场景 |
|---|---|---|---|
| 托管模式 | Managed | 默认无感,可疑时弹交互挑战 | 通用场景,推荐首选 |
| 非交互模式 | Non-interactive | 永远不弹交互,只显示一个图标 | 隐私敏感、不想打扰用户 |
| 隐身模式 | Invisible | 完全不可见,由代码触发验证 | 表单提交前自动校验、登录页 |
托管模式是默认值,也是 Cloudflare 官方推荐的。它会动态调整难度,可信访客零交互,可疑访客才看到挑战。非交互和隐身模式牺牲一部分准确率换取体验,需要更宽松地接受可疑流量。
隐式渲染嵌入示例
最简单的嵌入方式,三步搞定。先在页面 head 引入脚本
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
在表单里放一个容器
<form action="/login" method="post">
<input type="text" name="username" placeholder="用户名" />
<input type="password" name="password" placeholder="密码" />
<div class="cf-turnstile" data-sitekey="0x4AAAAAAABBBBBBBBBBBB"></div>
<button type="submit">登录</button>
</form>
脚本加载后会扫描所有带 cf-turnstile 类的元素,自动渲染 widget。验证通过后 Turnstile 会在容器里插入一个名为 cf-turnstile-response 的隐藏 input,表单提交时随其他字段一起发到后端,这就是后端要校验的 token。
常用 data-attribute
| 属性 | 作用 |
|---|---|
| data-sitekey | 站点密钥,必填 |
| data-theme | 主题,light 或 dark |
| data-size | 尺寸,normal、compact 或 flexible |
| data-callback | 验证通过时调用的 JS 函数名 |
| data-expired-callback | token 过期时调用的 JS 函数名 |
| data-error-callback | 验证失败时调用的 JS 函数名 |
token 有效期大约 300 秒,过期后 widget 会自动刷新。如果你想在过期时给用户提示,就配 data-expired-callback。
显式渲染嵌入示例
单页应用更适合显式渲染。先给容器一个 id,不再加 cf-turnstile 类
<div id="turnstile-container"></div>
脚本依旧引入,但加上 ?render=explicit 参数,禁止自动渲染
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
在需要的时候调用 turnstile.render
const widgetId = turnstile.render('#turnstile-container', {
sitekey: '0x4AAAAAAABBBBBBBBBBBB',
theme: 'light',
size: 'normal',
callback: (token) => {
console.log('验证通过,token', token);
document.querySelector('input[name="cf-turnstile-response"]').value = token;
},
'expired-callback': () => {
console.log('token 过期,请重新验证');
turnstile.reset(widgetId);
},
'error-callback': (code) => {
console.error('验证失败,错误码', code);
},
});
render 返回一个 widgetId,后续可以用它做 reset 或 remove。表单校验失败、用户改了密码、token 过期这类场景都要 reset 让用户重新验证,否则后端会拿到一个无效 token。
几个常见踩坑
第一,域名白名单。Turnstile 会校验当前页面域名是否在站点配置里,本地开发要把 localhost 和 127.0.0.1 都加进去。生产环境记得把所有可能的子域名都加上,比如 www.example.com 和 example.com 是分开的。
第二,脚本加载顺序。隐式渲染依赖 api.js 加载完成,如果脚本 async 但表单提前提交,可能拿不到 token。可以在表单提交时先判断隐藏 input 是否有值,没有就阻止提交并提示。
第三,token 只能用一次。后端校验成功后 token 立即失效,不能用同一个 token 校验两次。前端如果做了重试逻辑,每次重试前都要 reset 重新拿 token。
第四,size 选择。compact 尺寸适合移动端窄空间,但显示效果略差。flexible 是较新的模式,会按容器宽度自适应,体验更好但要确认浏览器兼容性。
小结
Turnstile 的前端嵌入核心是三件事,站点密钥标识站点、widget 是页面上那个验证组件、渲染模式决定用户看到什么。隐式渲染适合静态页面,加个类名就行;显式渲染适合单页应用,能精确控制挂载时机。三种模式里托管模式最常用,体验和准确率平衡最好。表单提交时记得把 cf-turnstile-response 这个隐藏字段一起带去后端,下一篇就讲后端怎么校验它。
下一篇 后端校验与集成