Update SproutGate

This commit is contained in:
2026-05-13 12:19:36 +08:00
parent f7db8aa053
commit a37b92e144
51 changed files with 12034 additions and 254 deletions

View File

@@ -0,0 +1,185 @@
# 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 节的通用步骤即可;具体路由与配置项以各自系统为准。