> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Turnstile 反爬

> 用 Cloudflare Turnstile 守住注册、忘记密码、订阅表单——隐私友好、免费、不下 cookie。

公开表单(注册、忘记密码、订阅)上线后几小时内就会被机器人扫描 —— 虚假注册污染你的
audience、密码重置邮件刷爆你的邮件配额、Stripe 风控评分被拖低。vibestrap 用
[Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/) 守住这三个
表单 —— 一个 CAPTCHA 替代品,隐私友好、任意流量免费、不下 cookie。

Widget 已经接到注册、忘记密码、订阅这三个表单上;服务端校验工具在
`src/lib/turnstile.ts`,会在 action 跑之前先验 token。如果 env 没配,校验会优雅地
返回 `{ ok: true, skipped: true }`,开发环境和未配置部署都能照常工作。

## 为啥选 Turnstile（而不是 reCAPTCHA）

* **隐私友好**。不下 cookie、不接 Google 跟踪，开箱即合 GDPR。
* **任意流量免费**——没有用量上限。
* **用户摩擦小**——大部分挑战都是隐形的（managed 模式）。
* **Cloudflare 生态**——已经走 Cloudflare 代理的话，没什么好犹豫的。

## 前置条件

* Cloudflare 账号（免费版就够）。
* 在 [dash.cloudflare.com](https://dash.cloudflare.com/) → Turnstile → Add Site
  把域名注册成 Turnstile site。
* 从该站点设置页拿到 site key 和 secret key。

## 一步步配置

1. 在 Cloudflare dashboard 创建一个 Turnstile site。把生产域名加进去，
   再加上 `localhost` 用于本地开发。模式选 "Managed"（Cloudflare 自己决定要不要挑战，
   用户体验最好）。

2. 在 `.env.local` 配两个 env 变量：

   ```bash theme={null}
   NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAA...     # 公开，会打到客户端
   TURNSTILE_SECRET_KEY=0x4AAAAAAA...                # 服务端，永不暴露
   ```

3. 确认 `src/config/site.ts` 的 feature flag（默认就是开的）：

   ```ts theme={null}
   features: {
     enableTurnstile: true,
   },
   ```

4. **重启 `pnpm dev`**。Widget 会出现在注册、忘记密码、订阅表单上。
   Server action 会自动调 `verifyTurnstile(token)`，token 缺失或非法直接拒绝。

## 验证生效

1. 无痕打开 `/register`——能看到 Turnstile widget（通常是隐形的，或一闪 "verifying"）。
2. 打开 devtools → Network。提交时表单会带 `cf-turnstile-response` 字段。
   Server action 内部调
   `https://challenges.cloudflare.com/turnstile/v0/siteverify`，浏览器侧看不到这个外联请求。
3. 把 `NEXT_PUBLIC_TURNSTILE_SITE_KEY` 删掉再试——widget 消失。服务端这边
   `turnstileEnabled()` 返回 `false`，校验直接 skip（返回 `{ ok: true, skipped: true }`）。
4. 用 devtools 篡改 token → server action 报错，表单不提交。

## 常见坑

1. **token 只能用一次**。如果 server action 内部用同一个 token 重试（比如 DB 抖动后重试），
   第二次就会失败。要么提交失败时重置 widget，要么在 action 入口短路。
2. **token 大约 5 分钟过期**。长寿命表单页（多步注册之类）提交前要刷新一下 widget。
   调 `turnstile.render` 时设个 `expired-callback`，Turnstile JS 会自动续。
3. **不要只信客户端**。绿色对勾没有服务端校验就是表演。`verifyTurnstile()` 才是真正在
   保护你的，自己写 server action 时千万别绕开。
4. **生产忘配 token**。常见原因：`TURNSTILE_SECRET_KEY` 配了，但
   `NEXT_PUBLIC_TURNSTILE_SITE_KEY` 没部署（托管平台的 env 没加）。后果：widget 不出现 →
   没有 token → 服务端拒绝所有注册。两个 env 必须一起部署。
5. **localhost 没加白名单**。要把 `localhost`（如果你用 `127.0.0.1` 也加上）
   加到 Turnstile site 的域名列表，否则本地注册会静默失败。

## 怎么接的

```ts theme={null}
// src/lib/turnstile.ts（节选）
export async function verifyTurnstile(token: string | undefined | null) {
  if (!siteConfig.features.enableTurnstile || !env.TURNSTILE_SECRET_KEY) {
    return { ok: true, skipped: true };
  }
  if (!token) return { ok: false, skipped: false, errors: ['missing-token'] };
  // POST 到 Cloudflare siteverify，把结果解析返回。
}
```

Server action 第一件事就是调 `verifyTurnstile(input.cfTurnstileToken)`，`!ok` 直接短路。
要给新表单加保护：客户端渲染 widget，把 token 通过 action 的 input schema 传过来，
action body 里调一下 `verifyTurnstile` 即可。

## 怎么关掉

二选一：

* 把 `siteConfig.features.enableTurnstile = false`（编译期关），或
* 两个 env 变量都不配（运行时 skip）。

任意一种都会让 `verifyTurnstile` 返回 `{ ok: true, skipped: true }`，action 后面照常走。

## 官方文档

* Turnstile 文档：[developers.cloudflare.com/turnstile](https://developers.cloudflare.com/turnstile/)
* 服务端校验：[developers.cloudflare.com/turnstile/get-started/server-side-validation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)
* 测试 key：[developers.cloudflare.com/turnstile/troubleshooting/testing](https://developers.cloudflare.com/turnstile/troubleshooting/testing/)
