185 lines
8.8 KiB
Markdown
185 lines
8.8 KiB
Markdown
# 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 节的通用步骤即可;具体路由与配置项以各自系统为准。 |