配置 Turnstile

了解首次部署如何使用测试密钥,再为正式域名创建 Turnstile Widget、替换密钥并验证人机验证流程。

Turnstile 用来拦截机器人提交和恶意尝试。Saavo 模板部分功能高度依赖 Turnstile 的相关功能,默认情况下已经接好了浏览器端 Widget 和服务端 Siteverify 验证,你不需要再复制 Cloudflare 示例代码。

首次部署时可以先沿用 Cloudflare 官方测试密钥,把 Worker 和其他资源部署起来;确定正式域名后,再创建 Widget 并替换成真实密钥。这样不用为了一个尚未确定的域名卡住第一次部署。

测试密钥只能用于临时验证

Cloudflare 测试密钥可以在任何 Hostname 上使用,但不会提供真实的人机识别能力。用它部署到 workers.dev 没有问题,正式向用户开放注册、登录或支持表单之前,必须换成正式 Widget 生成的密钥。

什么时候配置

使用下面的命令创建项目时,Saavo CLI 不会询问 Turnstile 密钥:

npx saavo-cli@latest create my-project

项目创建完成后,本地 .env 会保留测试密钥。准备第一次部署到 Cloudflare 时运行:

npx wrangler login
npm run deploy:init

deploy:init 会完成 Cloudflare 账户选择、Worker 和资源命名、生产环境变量收集以及首次部署。只要 config/deploy.ts 中没有关闭 Turnstile,交互过程就会要求输入:

Cloudflare Turnstile site key:
Cloudflare Turnstile secret key:

这两项不能留空,但部署脚本不会区分测试密钥和真实密钥。暂时只有 workers.dev 地址时,可以输入 example.vars 中的官方测试组合;已经确定正式域名,也已经创建 Widget,则直接输入真实密钥。两项值都会写入 .env.production,不会改动本地 .env。

正式开放前准备好 Hostname

创建 Widget 时,Cloudflare 至少要求填写一个允许使用它的 Hostname。通常填写产品准备使用的正式域名,例如:

example.com

只填写主机名,不要带协议、端口或路径。下面这些写法都不对:

https://example.com
example.com:443
example.com/login
*.example.com

Cloudflare 当前的规则是:填写 example.com 后,它的所有子域名也能使用这个 Widget;如果只填写 app.example.com,则不会自动包含 example.com 或其他同级子域名。完整规则见 Hostname management。

Saavo 第一次部署会先使用 workers.dev 地址。只想确认部署链路时,直接使用官方测试密钥即可,不必为了临时地址提前创建正式 Widget。准备接入自定义域名后,再把正式域名加入 Hostname Management 并生成真实密钥。

如果你确实要把 workers.dev 地址长期提供给用户,也应为实际 Hostname 配置正式 Widget,而不是一直使用测试密钥。Cloudflare 官方文档没有规定 workers.dev 地址一律不能创建 Widget,是否可添加应以 Dashboard 当时的校验结果为准。

Turnstile 列表页

创建 Widget

  1. 登录 Cloudflare Dashboard,切换到部署 Saavo 的账户。
  2. 打开侧边栏中的 Application security → Turnstile。
  3. 点击右上角 Add widget manually 按钮。
  4. 在 Widget name 中填写容易辨认的名称,例如 saavo-production。
  5. 在 Hostname Management 中添加准备使用的正式域名。
  6. 在 Widget Mode 中选择 Managed。
  7. 保持 Pre-clearance 的默认设置,点击 Create。

创建 Widget 表单

Managed 是 Cloudflare 推荐的默认模式。它会根据访问风险决定是否需要用户交互,正常用户多数时候不需要额外操作。

对于表单最后的选项 Pre-clearance 功能暂时可以忽略。Saavo 不依赖 Pre-clearance 产生的 cf_clearance Cookie,因此没有同时配置 WAF Challenge 的情况下,不用开启这项功能。

Cloudflare 的最新 Dashboard 操作说明见 Create and manage widgets。界面名称以后可能会调整,但需要填写的核心内容仍然是名称、Hostname 和 Widget Mode。

保存两把密钥

Widget 创建完成后,页面会显示两项内容:

Cloudflare 字段Saavo 环境变量是否可以公开
Site KeyCLOUDFLARE_TURNSTILE_SITE_KEY可以,会发送到浏览器
Secret KeyCLOUDFLARE_TURNSTILE_SECRET_KEY不可以,只能留在服务端

先把它们保存到密码管理器或团队使用的密钥管理工具。Secret Key 不要发到聊天群,不要写进 config/deploy.ts,也不要提交到 Git。

Site Key 和 Secret Key 必须成对使用

测试 Site Key 要搭配测试 Secret Key,生产 Site Key 要搭配同一个 Widget 生成的生产 Secret Key。混用之后,页面可能仍能显示验证框,但服务端一定无法通过验证。

本地测试密钥无需改动

从 example.vars 复制出来的 .env 已经包含测试密钥,无需改动:

CLOUDFLARE_TURNSTILE_SITE_KEY="3x00000000000000000000FF"
CLOUDFLARE_TURNSTILE_SECRET_KEY="1x0000000000000000000000000000000AA"

这不是需要替换的占位符,而是 Cloudflare 公开提供的测试组合。当前 Site Key 会强制显示一次交互式验证,便于检查界面;Secret Key 会让对应的测试 Token 通过服务端验证。

测试密钥可以用于任何开发域名,生产密钥则会检查 Widget 中允许的 Hostname。Cloudflare 也明确建议不要把 localhost 和 127.0.0.1 加入生产 Widget,测试规则和其它测试组合可查阅 Test your Turnstile implementation。

配置远程部署

不需要自己执行 wrangler secret put。首次运行 npm run deploy:init 时,在交互过程中输入成对的 Site Key 和 Secret Key。没有正式域名时可以输入本文前面的测试组合;已经创建 Widget 时则输入真实密钥。命令会先列出准备写入的远程环境变量:

Production environment changes:
- set CLOUDFLARE_TURNSTILE_SITE_KEY
- set CLOUDFLARE_TURNSTILE_SECRET_KEY

确认 Write these production environment changes? 后,两项值会写入项目根目录的 .env.production。如果这个文件还不存在,部署脚本会以 example.vars 为字段清单创建它;如果已经存在,则保留其中已经填写的值。部署脚本只检查两项是否为空,不会因为它们是 Cloudflare 测试密钥而中止部署。

最终文件中应当包含:

CLOUDFLARE_TURNSTILE_SITE_KEY="测试 Site Key 或真实 Site Key"
CLOUDFLARE_TURNSTILE_SECRET_KEY="与 Site Key 配对的 Secret Key"

.env.production 是 Saavo 部署流程唯一读取的生产环境变量文件,并且已经被 .gitignore 排除。部署时,脚本会把 Site Key 作为公开的 Worker 变量,把 Secret Key 作为加密 Secret 交给 Wrangler;中间使用的临时 Secrets 文件会在命令结束后删除。

两个环境文件各管各的

.env 服务于本地开发,可以一直保留测试密钥;.env.production 服务于远程部署,第一次部署时可以暂存测试密钥,正式开放前再换成真实密钥。不要把真实 Secret Key 复制到本地环境,也不要提交这两个文件。

如果中途退出或没有保存

在确认写入 .env.production 之前退出命令,刚才输入的值不会保存。处理方法取决于项目是否已经完成首次部署:

  • 还没有完成首次部署:重新运行 npm run deploy:init,再次输入成对的两把密钥;
  • 已经完成首次部署:直接修改 .env.production 中的两项值,然后运行 npm run deploy:update;
  • 想在部署前提前准备:可以手动创建或编辑 .env.production,写入两把密钥,再运行 npm run deploy:init。如果文件是手动新建的,还要确保其中的 SAAS_SECRET 与本地 .env 完全一致。

不要只在 Cloudflare 后台或通过 wrangler secret put 修改远程值。后续 npm run deploy:update 仍然以 .env.production 为准,只改远程环境会让项目记录与实际部署状态不一致。

修改完成后,尚未首次部署的项目继续运行 npm run deploy:init;已经初始化过的项目运行 npm run deploy:update。这两个命令都会在真正部署前检查配置并同步 Wrangler Secret,但只会检查密钥是否填写,不会判断它是不是测试密钥。

换成自定义域名后替换测试密钥

确定正式域名后,按下面的顺序处理:

  1. 在 Cloudflare 创建 Turnstile Widget,把正式域名加入 Hostname Management;
  2. 将 Widget 生成的 Site Key 和 Secret Key 写入 .env.production;
  3. 如果自定义域名还没有绑定,运行 npm run domain:set -- https://app.example.com;这个命令会连同新密钥一起重新部署;
  4. 如果域名已经绑定,只需运行 npm run deploy:update。

部署完成后,用正式域名实际提交一次注册或登录表单。确认服务端验证通过,再把站点开放给真实用户。

了解模板的验证策略

Saavo 默认不会在每一次登录和注册时都显示验证框。config/deploy.ts 中的配置是:

auth: {
    useTurnstile: {
        threshold: 2,
        interval: '1h',
    },
},

模板会按认证入口记录风险分数。达到 threshold 后,下一次请求才要求完成 Turnstile;验证成功后风险会下降,一段时间没有活动也会重置。这样可以减少正常用户每次登录都要验证的打扰。

如果调试时希望认证页面始终要求验证,可以暂时改为:

useTurnstile: {
    threshold: 0,
    interval: '1h',
},

测试完成后再改回默认值。设置为 false 只会关闭注册登录相关页面中的 Turnstile,不是整个项目的总开关;支持工单等明确要求人机验证的表单仍可能继续使用 Turnstile。

常见问题

页面一直不显示验证框

如果是登录或注册页面,先检查默认的自适应阈值。风险分数没有达到 threshold 时不显示验证框是正常行为,不代表 Site Key 没有生效。调试时可以临时把阈值改成 0。

本地正常,正式域名无法加载

先回到 Widget 的 Settings → Hostname Management,检查浏览器地址栏里的实际 Hostname 是否在允许范围内。只填域名,不要填写协议、端口和路径。刚修改 Hostname 后也要保存设置。

页面能显示,提交后提示验证失败

通常是 Site Key 和 Secret Key 不属于同一个 Widget,或者测试密钥与生产密钥混用了。重新核对 .env.production 中的两项变量,然后运行 npm run deploy:update。

偶尔出现 timeout-or-duplicate

Token 已经超过五分钟,或者同一个 Token 被提交了两次。刷新验证框后重新提交,不要缓存或重复使用旧 Token。

修改环境变量后没有变化

本地测试读取 .env,修改后要重启 npm run dev。远程部署读取 .env.production,修改后要运行 npm run deploy:update。改错文件不会自动同步到另一个环境。

完成这些检查后,Turnstile 就已经接通。后续更换域名时,记得先更新 Widget 的 Hostname Management,再切换站点流量。