# 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 ``` 全局会暴露 `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 节的通用步骤即可;具体路由与配置项以各自系统为准。