# Cloudflare Turnstile 网站接入指南

本文说明如何在任意网站中集成 [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) 人机验证：从控制台拿密钥、前端展示控件、把一次性 token 交给后端、由服务端向 Cloudflare 校验。内容适用于自建站点与 API，不限定具体技术栈，文末可对照本仓库中的实现。

---

## 1. 概念与流程


| 项                   | 说明                                                                                     |
| ------------------- | -------------------------------------------------------------------------------------- |
| **Site Key**        | 公钥，可出现在前端代码或 HTML 中。                                                                   |
| **Secret Key**      | 私钥，**仅**能用于你的服务端调用校验接口。                                                                |
| **Token（response）** | 用户通过验证后，Turnstile 在浏览器中生成的一串临时字符串，需在你自己的业务请求里随表单/JSON 提交到**你的后端**。                     |
| **二次校验**            | 前端 token **不能**单独作为“已通过人机验证”的依据；必须由服务器使用 Secret Key 调用 Cloudflare 的 `siteverify` 接口确认。 |


典型流程：

1. 页面加载 Cloudflare 提供的 JS，在指定 DOM 中渲染小组件。
2. 用户通过挑战后，前端拿到 `token`，写入状态或随登录/注册请求提交。
3. 你的后端在执行业务逻辑**之前**用 Secret Key + `token`（及可选的客户端 IP）请求 `siteverify`；`success: true` 才继续。
4. 每个 token 仅应用一次、且有时效；提交失败后应**重置**控件以获取新 token（见第 3 节）。

---

## 2. 在 Cloudflare 控制台创建站点

1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)，进入 **Turnstile**。
2. 选择 **Add widget** / 添加站点。
3. 填写网站域名、选择 **Managed**（或按需选择可见性/交互方式）。
4. 创建后得到 **Site Key** 与 **Secret Key**；为生产环境可单独为生产域名建 widget，勿把 Secret Key 写进仓库或静态资源。

**本地开发**：在 Turnstile 站点里把 `localhost` 及测试域名加入允许的主机名（与 Cloudflare 当前 UI 中「主机名」/域名列表一致），否则可能无法加载或校验失败。

---

## 3. 前端：加载脚本与渲染控件

### 3.1 官方脚本

推荐按文档使用官方脚本，例如**显式渲染**（`render=explicit`）便于在单页应用里把控件挂在指定容器上：

- 脚本地址：  
`https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit`

在页面中插入一次即可（注意避免重复注入同一脚本多份，除非你有意为之）。

```html
<script
  src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
  defer
></script>
```

全局会暴露 `window.turnstile`，主要方法包括：

- `turnstile.render(container, options)`：在 `container`（DOM 元素或选择器）内渲染，返回 `widgetId`。  
- `turnstile.reset(widgetId)`：同一会话内换发新 token（失败重试、重复提交时很有用）。  
- `turnstile.remove(widgetId)`：销毁实例。

### 3.2 常用 `render` 参数


| 参数                 | 含义                              |
| ------------------ | ------------------------------- |
| `sitekey`          | 必填，即 Site Key。                  |
| `callback`         | 验证通过时回调，参数为 `token` 字符串。        |
| `expired-callback` | token 过期时。                      |
| `error-callback`   | 发生错误时。                          |
| `theme`            | 可选 `light` / `dark` / `auto` 等。 |


建议在 `expired-callback` 与 `error-callback` 里把本地产出的 token 清空，避免用失效 token 提交。

### 3.3 把 token 交给你的后端

验证通过后，在 `callback` 中保存 `token`（如 React 的 state、Vue 的 ref）。用户点击「登录/提交」时，将 **同一请求** 中的其它字段与 `turnstileToken`（或你命名的字段名）一起发给**你自己的** API。字段名仅前后端一致即可，例如 JSON：

```json
{
  "username": "user",
  "password": "…",
  "turnstileToken": "0.xxx…"
}
```

**注意**：

- 未启用 Turnstile 的页面或测试环境，前后端要约定好是否“可不传 token”，避免生产误关校验。  
- 提交后若失败，应调用 `reset` 并清空本地 token，避免复用。

### 3.4 与 Content Security Policy (CSP) 的兼容

若站点启用了严格 CSP，需允许 Cloudflare 相关源，例如（按实际策略合并）：

- `script-src`：`https://challenges.cloudflare.com`  
- `frame-src` 或 `child-src`：同上（部分实现会以 iframe 呈现挑战）  
- 有时还需 `https://challenges.cloudflare.com/cdn-cgi/...` 等，以浏览器控制台与官方文档为准。

---

## 4. 后端：siteverify 校验

在**执行业务逻辑之前**用服务器端向 Cloudflare 验证 token。不应信任客户端声称的“已验证”。

### 4.1 端点

```
POST https://challenges.cloudflare.com/turnstile/v0/siteverify
```

使用 `application/x-www-form-urlencoded` 或等价的表单编码提交。

### 4.2 参数


| 参数         | 必填  | 说明                                    |
| ---------- | --- | ------------------------------------- |
| `secret`   | 是   | Secret Key。                           |
| `response` | 是   | 前端传来的 token。                          |
| `remoteip` | 否   | 发起请求的用户 IP，可与 Cloudflare 侧风控一致，建议有则传。 |


### 4.3 响应

JSON 中至少包含 `success`（布尔值）。`success` 为 `true` 才应继续登录、注册、发帖等业务。  

失败时 JSON 中常带有 `error-codes` 数组，便于排障，例如 `invalid-input-response`、`timeout-or-duplicate` 等（以[官方说明](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)为准）。

**实践建议**：

- 为 HTTP 客户端设置合理超时（如 8～15 秒），失败时对用户显示通用错误，**不要**在错误信息中泄露 Secret Key 或内部堆栈。  
- `siteverify` 应仅在服务端调用；不要从浏览器直接带 Secret 请求。

### 4.4 伪代码示例

```text
if secret_key is empty: return 500  # 未配置
if user_token is empty: return 400  # 请完成人机验证
POST form: secret, response, (optional) remoteip
if JSON.success != true: return 400  # 验证失败
# 再执行登录/注册等逻辑
```

语言无关：Go 可用 `http.PostForm`，Node 可用 `fetch` + `URLSearchParams`，Python 可用 `httpx`/`requests` 的 `data=` 等。

---

## 5. 安全与产品注意点

- **Site Key 可公开，Secret Key 绝不可进前端、公开仓库、日志。** 使用环境变量或密钥管理。  
- **每次敏感操作**若需要防护，可要求新的 token，而不是长缓存同一条 token。  
- 与**速率限制、账户锁定、异常 IP 检测**等组合，而不是只依赖 Turnstile。  
- 若使用**反向代理**，`remoteip` 应取真实客户端 IP（与 Turnstile 可见一致），与信任代理头配置统一。  
- 多环境（dev/staging/prod）使用不同 widget 与密钥，避免误用。

---

## 6. 排障简表


| 现象                  | 可能原因                                             |
| ------------------- | ------------------------------------------------ |
| 前端不显示控件             | 域名未在 Turnstile 站点中、脚本被 CSP/广告拦截、脚本未加载完就 `render` |
| 总是 `error-callback` | Site Key 错误、域名校验失败、网络问题                          |
| `siteverify` 失败     | token 已用过/过期、Secret Key 错误、时间偏差过大、请求不是 POST 表单   |
| 生产正常、本地失败           | 未把 localhost 加入允许主机名、混用不同环境的 Key                 |


更完整的错误码与行为以 Cloudflare 官方文档为准：  
[https://developers.cloudflare.com/turnstile/](https://developers.cloudflare.com/turnstile/)

---

## 7. 与本项目（SproutGate）的对应关系（参考）

本仓库中可作为对照的实现位置（不保证与上文逐字相同，以代码为准）：

- 前端组件：`sproutgate-frontend/src/components/oauth/TurnstileWidget.jsx`（显式 `render`、token 回调、`reset`）。  
- 前端在登录/注册中附带 `turnstileToken`：`UserPortal.jsx`、`UserPortalAuthSection.jsx`。  
- 后端校验：`sproutgate-backend/internal/handlers/turnstile.go` 中 `verifyTurnstileToken` 及 `turnstileVerifyURL`。  
- 管理端配置 Site/Secret 与开关：`/api/admin/turnstile`、存储层 `TurnstileConfig`。

第三方站点只需遵守本文第 1～5 节的通用步骤即可；具体路由与配置项以各自系统为准。