src/lib/turnstile.ts,会在 action 跑之前先验 token。如果 env 没配,
校验会优雅地返回 { ok: true, skipped: true },开发环境和未配置部署都能照常工作。
为啥选 Turnstile(而不是 reCAPTCHA)
- 隐私友好。不下 cookie、不接 Google 跟踪,开箱即合 GDPR。
- 任意流量免费——没有用量上限。
- 用户摩擦小——大部分挑战都是隐形的(managed 模式)。
- Cloudflare 生态——已经走 Cloudflare 代理的话,没什么好犹豫的。
前置条件
- Cloudflare 账号(免费版就够)。
- 在 dash.cloudflare.com → Turnstile → Add Site 把域名注册成 Turnstile site。
- 从该站点设置页拿到 site key 和 secret key。
一步步配置
-
在 Cloudflare dashboard 创建一个 Turnstile site。把生产域名加进去,
再加上
localhost用于本地开发。模式选 “Managed”(Cloudflare 自己决定要不要挑战, 用户体验最好)。 -
在
.env.local配两个 env 变量: -
确认
src/config/site.ts的 feature flag(默认就是开的): -
重启
pnpm dev。Widget 会出现在注册、忘记密码、订阅表单上。 Server action 会自动调verifyTurnstile(token),token 缺失或非法直接拒绝。
验证生效
- 无痕打开
/register——能看到 Turnstile widget(通常是隐形的,或一闪 “verifying”)。 - 打开 devtools → Network。提交时表单会带
cf-turnstile-response字段。 Server action 内部调https://challenges.cloudflare.com/turnstile/v0/siteverify,浏览器侧看不到这个外联请求。 - 把
NEXT_PUBLIC_TURNSTILE_SITE_KEY删掉再试——widget 消失。服务端这边turnstileEnabled()返回false,校验直接 skip(返回{ ok: true, skipped: true })。 - 用 devtools 篡改 token → server action 报错,表单不提交。
常见坑
- token 只能用一次。如果 server action 内部用同一个 token 重试(比如 DB 抖动后重试), 第二次就会失败。要么提交失败时重置 widget,要么在 action 入口短路。
- token 大约 5 分钟过期。长寿命表单页(多步注册之类)提交前要刷新一下 widget。
调
turnstile.render时设个expired-callback,Turnstile JS 会自动续。 - 不要只信客户端。绿色对勾没有服务端校验就是表演。
verifyTurnstile()才是真正在 保护你的,自己写 server action 时千万别绕开。 - 生产忘配 token。常见原因:
TURNSTILE_SECRET_KEY配了,但NEXT_PUBLIC_TURNSTILE_SITE_KEY没部署(托管平台的 env 没加)。后果:widget 不出现 → 没有 token → 服务端拒绝所有注册。两个 env 必须一起部署。 - localhost 没加白名单。要把
localhost(如果你用127.0.0.1也加上) 加到 Turnstile site 的域名列表,否则本地注册会静默失败。
怎么接的
verifyTurnstile(input.cfTurnstileToken),!ok 直接短路。
要给新表单加保护:客户端渲染 widget,把 token 通过 action 的 input schema 传过来,
action body 里调一下 verifyTurnstile 即可。
怎么关掉
二选一:- 把
siteConfig.features.enableTurnstile = false(编译期关),或 - 两个 env 变量都不配(运行时 skip)。
verifyTurnstile 返回 { ok: true, skipped: true },action 后面照常走。