Files
SproutGate/docs/cloudflare-turnstile.md
2026-05-13 12:19:36 +08:00

185 lines
8.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Cloudflare Turnstile 网站接入指南
本文说明如何在任意网站中集成 [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) 人机验证:从控制台拿密钥、前端展示控件、把一次性 token 交给后端、由服务端向 Cloudflare 校验。内容适用于自建站点与 API不限定具体技术栈文末可对照本仓库中的实现。
---
## 1. 概念与流程
| 项 | 说明 |
| ------------------- | -------------------------------------------------------------------------------------- |
| **Site Key** | 公钥,可出现在前端代码或 HTML 中。 |
| **Secret Key** | 私钥,**仅**能用于你的服务端调用校验接口。 |
| **Tokenresponse** | 用户通过验证后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 客户端设置合理超时(如 815 秒),失败时对用户显示通用错误,**不要**在错误信息中泄露 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`
第三方站点只需遵守本文第 15 节的通用步骤即可;具体路由与配置项以各自系统为准。