首页
/ Next.js 集成 Cloudflare Turnstile 实战:隐式/显式渲染与 API 路由服务端校验详解

Next.js 集成 Cloudflare Turnstile 实战:隐式/显式渲染与 API 路由服务端校验详解

2026-09-06 14:58:56作者:邓越浪Henry

本文基于 Next.js 仓库中的官方示例 examples/cloudflare-turnstile 展开,完整讲解如何在一个 Next.js 项目中接入 Cloudflare Turnstile 智能人机验证:从 Cloudflare 后台获取密钥、配置环境变量,到隐式(自动)渲染与显式(API)渲染两种前端集成方式,再到通过 Next.js API 路由完成服务端的 token 校验。读完后,你能够在自己的 Next.js 项目中落地一套"无感验证 + 服务端校验"的完整人机防护方案。

什么是 Cloudflare Turnstile

Turnstile 是 Cloudflare 提供的"智能 CAPTCHA 替代方案"。与普通验证码(图形点选、滑块拖动等)不同,它具备两个核心特性:

  • 无需经过 Cloudflare 流量中转:可以嵌入任何网站,不要求站点本身的流量走 Cloudflare 网络;
  • 可"无感"工作:在风控判断访客可信时,可以不向访客展示任何验证码界面,从而减少对正常用户的干扰。

其工作原理是两段式的:

  1. 客户端:页面加载 Turnstile 官方脚本,渲染一个验证组件。访客交互(或直接通过)后,组件生成一个验证 token,并以 cf-turnstile-response 字段随表单提交;
  2. 服务端:后端拿到 token 后,调用 Cloudflare 的 siteverify 接口,携带站点 Secret Key 换取验证结果,以此决定是否放行请求。

示例项目正是用两个页面分别演示了这两种渲染方式,再用一个 API 路由演示了服务端校验,代码量非常精简,适合直接作为接入模板。

项目结构与获取方式

示例位于仓库的 examples/cloudflare-turnstile/ 目录,整体结构如下:

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 给出的操作步骤:

  1. 登录 Cloudflare 控制台,选择你的账号;
  2. 进入 Turnstile 管理页;
  3. 点击 Add a site,填写表单(通常包含站点域名与验证码模式);
  4. 创建完成后,复制你的 Site KeySecret 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_KEY
  • CLOUDFLARE_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}
      />
      <form method="POST" action="/api/handler">
        <h2>Dummy Login Demo</h2>
        <div
          className="cf-turnstile checkbox"
          data-sitekey={process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY}
        />
        <button type="submit">Sign in</button>
        <p>
          Go to the <a href="/explicit">explicit render demo</a>
        </p>
      </form>
    </main>
  );
}

关键细节有三个:

  1. <Script> 加载官方脚本:使用 Next.js 内置的 next/script 组件引入 https://challenges.cloudflare.com/turnstile/v0/api.js,并加上 async / defer 避免阻塞页面解析。相比在 HTML 里硬编码 <script> 标签,next/script 由 Next.js 统一管理加载时机,与框架的 hydration 流程兼容;
  2. cf-turnstile class 是自动渲染的触发条件:脚本扫描到 class="cf-turnstile"div 后自动渲染。data-sitekey 传入 NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY,由于该变量在构建时被内联,客户端可直接读到实际值;
  3. 表单以 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}
      />
      <form method="POST" action="/api/handler">
        <h2>Dummy Login Demo</h2>
        <div id="my-widget" className="checkbox" />
        <button type="submit">Sign in</button>
        <p>
          Go to the <a href="/implicit">implicit render demo</a>
        </p>
      </form>
    </main>
  );
}
  1. onload 查询参数挂接加载回调:脚本 URL 写作 api.js?onload=onloadTurnstileCallback,即 Turnstile SDK 加载完成后会自动调用名为 onloadTurnstileCallback 的全局函数。这里刻意不用 window.addEventListener("load", ...),而是利用官方 SDK 的 onload 参数,确保回调一定在 SDK 就绪之后触发,规避脚本加载时序问题;
  2. 在回调里调用 window.turnstile.render:第一个参数是容器(选择器字符串或 DOM 元素),第二个参数是渲染参数。示例中定义了 RenderParameters 类型,从源码结构看,其可接受的核心参数包括:
    • sitekey: string(必填):站点 Site Key;
    • theme?: "light" | "dark":组件主题,可控制明暗风格;
    • callback?(token: string):验证成功后的回调,参数即为生成的 token,可用于在 JS 层面直接拿到 token 做异步提交,而不必依赖表单隐藏字段;
  3. 容器是普通空 div<div id="my-widget" /> 不需要 cf-turnstile class,是否渲染完全由 render 调用决定。文件顶部的 declare globalwindow.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"演示,实际项目应在此处接上自己的登录/注册流程)。这里需要注意两个工程细节:

  1. token 校验必须在服务端做:前端提交的任何值都不可信,只有携带 Secret Key 调用 siteverify 得到的结果才能作为裁决依据;
  2. 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 的三个关键环节:

  1. 配置:Cloudflare 后台创建站点获取 Site Key / Secret Key,通过 .env.local 分别以 NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY(客户端可见)和 CLOUDFLARE_TURNSTILE_SECRET_KEY(仅 Node.js 侧可见)注入,前缀即作用域;
  2. 前端渲染:隐式渲染靠 cf-turnstile class 与 data-sitekey 自动完成;显式渲染通过 api.js?onload=... 回调中调用 window.turnstile.render,可传 sitekeythemecallback 等参数做精细控制;
  3. 服务端校验:API 路由将 secretresponse(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 只适用于本地开发。

登录后查看全文
热门项目推荐
相关项目推荐