Next.js 集成 Cloudflare Turnstile 实战:隐式/显式渲染与 API 路由服务端校验详解
本文基于 Next.js 仓库中的官方示例 examples/cloudflare-turnstile 展开,完整讲解如何在一个 Next.js 项目中接入 Cloudflare Turnstile 智能人机验证:从 Cloudflare 后台获取密钥、配置环境变量,到隐式(自动)渲染与显式(API)渲染两种前端集成方式,再到通过 Next.js API 路由完成服务端的 token 校验。读完后,你能够在自己的 Next.js 项目中落地一套"无感验证 + 服务端校验"的完整人机防护方案。
什么是 Cloudflare Turnstile
Turnstile 是 Cloudflare 提供的"智能 CAPTCHA 替代方案"。与普通验证码(图形点选、滑块拖动等)不同,它具备两个核心特性:
- 无需经过 Cloudflare 流量中转:可以嵌入任何网站,不要求站点本身的流量走 Cloudflare 网络;
- 可"无感"工作:在风控判断访客可信时,可以不向访客展示任何验证码界面,从而减少对正常用户的干扰。
其工作原理是两段式的:
- 客户端:页面加载 Turnstile 官方脚本,渲染一个验证组件。访客交互(或直接通过)后,组件生成一个验证 token,并以
cf-turnstile-response字段随表单提交; - 服务端:后端拿到 token 后,调用 Cloudflare 的
siteverify接口,携带站点 Secret Key 换取验证结果,以此决定是否放行请求。
示例项目正是用两个页面分别演示了这两种渲染方式,再用一个 API 路由演示了服务端校验,代码量非常精简,适合直接作为接入模板。
项目结构与获取方式
示例位于仓库的 examples/cloudflare-turnstile/ 目录,整体结构如下:
- pages/implicit.tsx:隐式渲染(自动渲染)演示页,也是默认首页;
- pages/explicit.tsx:显式渲染(手动调用
turnstile.render)演示页; - pages/api/handler.ts:服务端校验用的 API 路由;
- pages/_app.tsx:全局应用入口;
- next.config.js:路由改写配置;
- app.css:演示页的居中布局样式;
- .env.local.example:环境变量模板。
从 package.json 可以看到,该示例基于 Next.js(next: latest)+ React 18 + TypeScript,构建与运行脚本均为标准的 next dev / next build / next start。
按 README 的说明,使用 create-next-app 引导创建该示例的命令如下(npm / Yarn / pnpm 三选一):
npx create-next-app --example cloudflare-turnstile cloudflare-turnstile-app
yarn create next-app --example cloudflare-turnstile cloudflare-turnstile-app
pnpm create next-app --example cloudflare-turnstile cloudflare-turnstile-app
创建完成后即可 npm run dev 启动本地开发服务器(启动前需先完成下文的环境变量配置)。
配置 Cloudflare Turnstile
获取 Site Key 与 Secret Key
按 README 给出的操作步骤:
- 登录 Cloudflare 控制台,选择你的账号;
- 进入 Turnstile 管理页;
- 点击 Add a site,填写表单(通常包含站点域名与验证码模式);
- 创建完成后,复制你的 Site Key 和 Secret Key 备查。
两者分工明确:
| 密钥 | 用途 | 可见性 |
|---|---|---|
| Site Key | 前端渲染验证组件时标识"这是哪个站点" | 会下发到浏览器,属于公开信息 |
| Secret Key | 服务端调用 siteverify 校验 token 时证明"这是合法的站点后端" | 绝不可暴露到浏览器,只能存在于 Node.js 环境 |
配置环境变量
将示例目录下的 .env.local.example 复制为 .env.local:
cp .env.local.example .env.local
然后打开 .env.local,填入两个环境变量:
NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEYCLOUDFLARE_TURNSTILE_SECRET_KEY
.env.local.example 中对两者的注释写得很清楚:
# Public Environment variables that can be used in the browser.
NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY=
# Secret environment variables only available to Node.js
CLOUDFLARE_TURNSTILE_SECRET_KEY=
这正体现了 Next.js 环境变量的作用域规则:以 NEXT_PUBLIC_ 前缀开头的变量会被内联进客户端 bundle,可以在浏览器端读取(示例中用于渲染组件);不带该前缀的变量只在 Node.js 侧(服务端渲染与 API 路由)可用,不会泄露给浏览器(示例中用于服务端校验)。部署到云环境时,需在部署平台的 Environment Variables 中配置与 .env.local 一致的两项,尤其是 Secret Key 不能只放在本地文件里。
隐式渲染:一行 div 自动出组件
隐式渲染(implicit rendering)是最简单的接入方式:只需加载 Turnstile 官方脚本,并在页面中放置一个带特定 class 和 data-sitekey 属性的空 div,脚本加载完成后会自动将其替换为验证组件。
pages/implicit.tsx 的完整实现:
import Script from "next/script";
export default function ImplicitRender() {
return (
<main>
<Script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async={true}
defer={true}
/>
);
}
关键细节有三个:
<Script>加载官方脚本:使用 Next.js 内置的next/script组件引入https://challenges.cloudflare.com/turnstile/v0/api.js,并加上async/defer避免阻塞页面解析。相比在 HTML 里硬编码<script>标签,next/script由 Next.js 统一管理加载时机,与框架的 hydration 流程兼容;cf-turnstileclass 是自动渲染的触发条件:脚本扫描到class="cf-turnstile"的div后自动渲染。data-sitekey传入NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY,由于该变量在构建时被内联,客户端可直接读到实际值;- 表单以
cf-turnstile-response字段提交 token:验证组件渲染完成后,会在所属<form>中自动生成一个名为cf-turnstile-response的隐藏输入,用户点击 Sign in 时,token 随POST /api/handler一起提交——这一点在 pages/api/handler.ts 中从req.body["cf-turnstile-response"]取值,可以互相印证。
checkbox 这个 class 来自 app.css 中为 .checkbox 设定的 min-height: 70px,用于给验证组件预留固定高度、避免渲染时布局跳动。
显式渲染:用 window.turnstile.render 控制组件
显式渲染(explicit rendering)把"何时渲染、渲染到哪个容器、传什么参数"完全交给开发者控制,适合需要自定义主题、监听回调、动态创建/销毁组件的场景。
pages/explicit.tsx 的实现分三步:
import Script from "next/script";
type RenderParameters = {
sitekey: string;
theme?: "light" | "dark";
callback?(token: string): void;
};
declare global {
interface Window {
onloadTurnstileCallback(): void;
turnstile: {
render(container: string | HTMLElement, params: RenderParameters): void;
};
}
}
export default function ExplicitRender() {
return (
<main>
<Script id="cf-turnstile-callback">
{`window.onloadTurnstileCallback = function () {
window.turnstile.render('#my-widget', {
sitekey: '${process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY}',
})
}`}
</Script>
<Script
src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback"
async={true}
defer={true}
/>
);
}
- 用
onload查询参数挂接加载回调:脚本 URL 写作api.js?onload=onloadTurnstileCallback,即 Turnstile SDK 加载完成后会自动调用名为onloadTurnstileCallback的全局函数。这里刻意不用window.addEventListener("load", ...),而是利用官方 SDK 的onload参数,确保回调一定在 SDK 就绪之后触发,规避脚本加载时序问题; - 在回调里调用
window.turnstile.render:第一个参数是容器(选择器字符串或 DOM 元素),第二个参数是渲染参数。示例中定义了RenderParameters类型,从源码结构看,其可接受的核心参数包括:sitekey: string(必填):站点 Site Key;theme?: "light" | "dark":组件主题,可控制明暗风格;callback?(token: string):验证成功后的回调,参数即为生成的 token,可用于在 JS 层面直接拿到 token 做异步提交,而不必依赖表单隐藏字段;
- 容器是普通空 div:
<div id="my-widget" />不需要cf-turnstileclass,是否渲染完全由render调用决定。文件顶部的declare global为window.turnstile补充了 TypeScript 声明,使render调用具备类型提示,这是显式渲染模式下保持类型体验的小技巧。
两种方式对比:隐式渲染适合"标准登录/注册表单"这类快速接入场景;显式渲染适合需要在组件生命周期内执行自定义逻辑(例如拿到 token 后立即发起 fetch、根据 theme 跟随深色模式切换、在表单提交前手动 turnstile.reset() 等)的场景。
服务端校验:API 路由与 siteverify 接口
客户端拿到的 token 只应被视为"待验证凭证",真正的裁决在服务端完成。示例的服务端逻辑全部在 pages/api/handler.ts 中,完整代码如下:
import type { NextApiRequest, NextApiResponse } from "next";
export default async function Handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const form = new URLSearchParams();
form.append("secret", process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY);
form.append("response", req.body["cf-turnstile-response"]);
form.append("remoteip", req.headers["x-forwarded-for"] as string);
const result = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ method: "POST", body: form },
);
const json = await result.json();
res.status(result.status).json(json);
}
逐行拆解这段实现:
secret:取自process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY。由于该变量没有NEXT_PUBLIC_前缀,只在 Node.js 侧可读,天然不会进入客户端 bundle,满足"Secret Key 不出服务端"的安全要求;response:即客户端表单提交的cf-turnstile-response,也就是要验证的 token;remoteip:从x-forwarded-for请求头取出访客 IP。在反向代理或云托管环境(如示例所面向的 Vercel 部署)中,应用直接拿到的连接地址通常是代理层地址,需要借助x-forwarded-for才能获得真实访客 IP,供 Cloudflare 风控参考;siteverify端点:https://challenges.cloudflare.com/turnstile/v0/siteverify是 Cloudflare 官方的验证入口,采用application/x-www-form-urlencoded形式 POST(URLSearchParams序列化恰好就是该格式)。
最后 res.status(result.status).json(json) 把 Cloudflare 的响应原样透传给前端,前端可据此判断 success 等字段决定后续业务逻辑(示例中即"Dummy Login"演示,实际项目应在此处接上自己的登录/注册流程)。这里需要注意两个工程细节:
- token 校验必须在服务端做:前端提交的任何值都不可信,只有携带 Secret Key 调用 siteverify 得到的结果才能作为裁决依据;
- API 路由需保证
req.body可用:Next.js 的 API 路由默认会对 JSON 请求体做解析;本示例表单是默认的浏览器表单提交(application/x-www-form-urlencoded),字段cf-turnstile-response会被解析进req.body。如果改为前端 fetch 提交 JSON,则应使用Content-Type: application/json,取值字段名保持一致即可。
路由组织:把隐式渲染设为首页
next.config.js 只做了路由改写:
module.exports = {
async rewrites() {
return [
{
source: "/",
destination: "/implicit",
},
];
},
};
通过 rewrites() 将根路径 / 指向 /implicit 页面,使访客打开站点首页时默认看到隐式渲染演示;两个演示页之间又通过页内链接(/implicit ↔ /explicit)互相跳转,形成完整的对照体验。pages/_app.tsx 则是标准的全局入口,负责引入 app.css 并渲染路由组件,与 Turnstile 逻辑解耦。
小结与适用说明
这套示例完整覆盖了接入 Cloudflare Turnstile 的三个关键环节:
- 配置:Cloudflare 后台创建站点获取 Site Key / Secret Key,通过
.env.local分别以NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY(客户端可见)和CLOUDFLARE_TURNSTILE_SECRET_KEY(仅 Node.js 侧可见)注入,前缀即作用域; - 前端渲染:隐式渲染靠
cf-turnstileclass 与data-sitekey自动完成;显式渲染通过api.js?onload=...回调中调用window.turnstile.render,可传sitekey、theme、callback等参数做精细控制; - 服务端校验:API 路由将
secret、response(token)、remoteip以表单编码 POST 到siteverify端点,以响应结果为准放行请求。
适用前提与限制:示例基于 Next.js Pages Router 与 React 18,API 校验用的是 API 路由(pages/api);若你的项目使用 App Router,可将同样的 fetch 校验逻辑迁移到 Route Handler 中实现,核心参数与端点不变。部署时务必在托管平台的 Environment Variables 中配置这两项变量(尤其 Secret Key),仅依赖仓库中的 .env.local 只适用于本地开发。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00