跳到主要内容

《林一的 Cloudflare 通关记》第 10 篇-挡住那些"机器人"——Turnstile 验证码替代方案

· 阅读需 21 分钟

《林一的 Cloudflare 通关记》第 10 篇

林一盯着屏幕上的日志,脸色发绿。

故事引入

POST /api/users/register  200  -- username: test_3947, email: test_3947@bot.xyz
POST /api/users/register 200 -- username: test_3948, email: test_3948@bot.xyz
POST /api/users/register 200 -- username: test_3949, email: test_3949@bot.xyz
POST /api/users/register 200 -- username: test_3950, email: test_3950@bot.xyz
...

D1 数据库上线还不到 24 小时,users 表里已经塞了 4000 多条假数据——全是同一个 IP,以每秒 50 次的频率疯狂调注册接口。真正的用户只有 20 多个。

"林一,我刚才看了一下后台数据,"小王的消息又来了,"注册用户 4000 多,但活跃用户 0。你给我解释解释?"

林一硬着头皮去找老张:"张哥,注册接口被刷了,全是机器人。得加个验证码。"

"验证码……"老张叹了口气,"你是说那种'请选出所有包含红绿灯的图片'?还是那种歪歪扭扭的字母数字?"

林一苦笑:"我上周注册一个网站,连选了三轮红绿灯,第四轮还选错了,直接被锁了 10 分钟。我自己都被验证码挡在门外过。"

"那就别用传统验证码。"老张推了推眼镜,"Cloudflare 有 Turnstile——不需要用户找红绿灯,后台默默就验证了。"

"默默验证?用户什么都不用做?"

"大多数情况是这样的。来,我给你讲讲它背后的原理。"


技术讲解

传统验证码的"罪状"

如果你做过 Web 开发,大概率用过 Google 的 reCAPTCHA。它曾经是验证码界的事实标准,但这些年口碑越来越差,原因很简单:

用户体验差。reCAPTCHA v2 让用户识别红绿灯、斑马线、消防栓、楼梯……有时候选了七八轮还在继续。更坑的是,它的判定逻辑是个黑盒——你觉得选对了,它觉得你选错了。用户在登录页面反复尝试验证码的场景,比比皆是。

隐私问题严重。reCAPTCHA 会收集大量用户数据——浏览历史、点击行为、鼠标轨迹、设备指纹——然后发给 Google。2020 年有一项研究表明,解决一次 reCAPTCHA v2 大约需要消耗 500 毫秒的人类注意力,全球用户每天花在验证码上的时间累计起来非常惊人。更关键的是,这些数据被 Google 用于广告投放和模型训练,用户毫无知情权。

reCAPTCHA v3 虽然做到了"无交互",但它返回的是一个 0 到 1 的分数,需要开发者自己决定阈值——分数设高了误杀正常用户,设低了放进机器人。而且 v3 依赖 Google 的全局用户画像,如果你不用 Google 的服务、清了 Cookie,分数就会很低,又回到"被怀疑"的境地。

"国内也有不少验证码服务,"老张补充道,"比如腾讯防水墙、极验行为验证、阿里的人机验证。它们很多也走行为验证路线,体验比传统图片验证码好不少。但如果你已经在用 Cloudflare 生态,Turnstile 是最顺手的选择——免费、无感、和 Workers 无缝集成。"

Turnstile 的核心原理:不用红绿灯,靠什么判断?

"好了,重点来了。"老张在白板上写下三个关键词:Proof of Work、浏览器指纹、行为分析

"Turnstile 不让用户找红绿灯,那它凭什么判断访客是人还是机器?答案是:在后台跑一系列'隐形挑战',收集浏览器环境信号,综合判断。"

Proof of Work(工作量证明)

"第一个核心机制是 Proof of Work——工作量证明。"

"这不是区块链里的概念吗?"林一问。

"对,原理一样。Turnstile 给浏览器出一道计算难题——通常是找到一个特定哈希前缀的随机数。浏览器需要不断尝试不同的输入值,计算哈希,直到找到满足条件的解。"

老张画了个示意流程:

Cloudflare 下发挑战:
找到一个 nonce,使得 SHA256(challenge_id + nonce) 的前 N 位为 0

浏览器开始计算:
nonce = 0 → SHA256("abc123" + "0") = "7f3a..." ✗ 前缀不匹配
nonce = 1 → SHA256("abc123" + "1") = "2b8c..." ✗ 前缀不匹配
nonce = 2 → SHA256("abc123" + "2") = "000f..." ✓ 找到了!

浏览器提交:nonce = 2
Cloudflare 验证:SHA256("abc123" + "2") 确实前缀为 0 → 通过

"关键在于:这道题对真实浏览器来说不算难——几十毫秒就能算完。但对批量发请求的脚本机器人来说,每次请求都要算一遍,成本就上去了。如果攻击者想每秒发 50 个请求,就得同时跑 50 个 PoW 计算,CPU 开销陡增。"

"这就像超市发优惠券——每张券上印了一道算术题,结账时得算出答案才能用。对人来说两秒钟的事,但如果你想让机器人一次薅 1000 张券,算题的时间成本就不可忽视了。"

Proof of Work 哈希挑战流程

"除了 PoW,Turnstile 还会使用 Proof of Space(空间证明)等其他挑战类型,进一步增加自动化成本。"

浏览器指纹与环境探测

"第二个机制是浏览器指纹。Turnstile 会在浏览器里跑一系列 JavaScript 探测,收集环境信号:"

探测维度具体内容能识别什么
Web API 存在性navigator.webdriverwindow.chromeHeadless 浏览器、自动化工具
Canvas 指纹绘制特定图形,读取像素数据虚拟机、模拟器环境
WebGL 指纹GPU 渲染特征无头浏览器缺少 GPU 加速
屏幕特征分辨率、色深、像素比批量克隆的虚拟机
时区与语言Intl.DateTimeFormatIP 地理位置与浏览器设置是否一致
浏览器怪癖特定浏览器才有的行为差异伪造 User-Agent 的脚本

"真实浏览器的环境非常复杂——不同操作系统、不同浏览器版本、不同显卡,会产生细微但独特的指纹。机器人要么用 Headless 浏览器(指纹特征和真实浏览器不一样),要么用 HTTP 库直接发请求(根本没有浏览器环境),在指纹探测面前很容易暴露。"

"比如 navigator.webdriver 这个属性,在 Selenium、Puppeteer 驱动的浏览器中默认是 true,而正常用户浏览器里是 undefinedfalse。虽然高级机器人会尝试伪造,但 Turnstile 的探测不止这一项——它是一个信号矩阵,单项可以伪造,全部伪造且保持一致性很难。"

浏览器指纹探测矩阵

行为分析

"第三个机制是行为分析。Turnstile 会在页面加载后悄悄记录用户的交互行为:"

  • 鼠标移动轨迹(是否平滑、是否有自然的加速减速)
  • 点击位置分布(真实用户的点击位置有随机偏移,脚本通常精准点击中心)
  • 滚动行为(是否有阅读节奏)
  • 页面停留时间(是秒级提交还是毫秒级提交)
  • 键盘输入节奏(打字速度是否有波动)

"一个真人打开注册页面,通常先看两眼页面,移动鼠标到输入框,打字有快有慢,偶尔打错再删掉。而机器人呢?页面刚加载完,0.1 秒内填完所有字段,鼠标轨迹是直线——甚至没有鼠标轨迹,直接程序化提交。"

"Turnstile 把这三类信号——PoW、浏览器指纹、行为分析——综合起来打分。风险低的访客直接通过,什么都不用做;风险稍高的可能需要勾选一个复选框(Managed 模式);风险极高的会被直接拒绝。"

Turnstile 三大核心机制总览

"所以 Turnstile 的全称叫'智能 CAPTCHA 替代方案'——它不是没有验证,而是把验证从'用户做题'变成了'后台分析'。大多数真人用户完全无感,只有可疑流量才会被拦截或要求额外交互。"

Turnstile vs reCAPTCHA:详细对比

老张在白板上画了张对比表:

对比维度reCAPTCHA v2/v3Cloudflare Turnstile
用户体验v2 需要选图片,平均耗时 10-30 秒;v3 无交互但依赖阈值多数用户完全无感,少数低风险用户需勾选复选框
隐私保护收集大量用户行为数据发送给 Google,用于广告和模型训练不出售用户数据,不用于广告定向,有独立的隐私附加条款
依赖关系依赖 Google 全局画像,新用户/隐私用户分数低独立运行,不依赖第三方用户画像,不要求用户登录任何账号
集成复杂度需要申请 Google API Key,前端嵌脚本,后端调 Google API前端嵌脚本,后端调 Cloudflare API,流程几乎一样但更简单
是否需要走 CDN不需要不需要,即使你的网站不走 Cloudflare CDN 也能用
价格免费(但有用量限制和企业版收费)完全免费,不限量
无障碍支持有音频验证但体验一般WCAG 2.2 AA 合规
迁移成本支持从 reCAPTCHA/hCaptcha 平滑迁移,脚本替换即可

"最关键的两点:第一,Turnstile 完全免费,不限调用次数;第二,它不依赖 Cloudflare CDN——即使你的网站托管在别处,也能用 Turnstile。"

三种 Widget 模式

Turnstile 提供三种工作模式,对应不同的用户体验需求:

模式用户体验适用场景
Managed(推荐)根据风险等级自动决定:低风险完全无感,中风险显示复选框通用场景,平衡安全和体验
Non-Interactive始终显示一个带加载动画的 widget,但不需要用户交互想让用户看到"正在验证"但不增加操作
Invisible完全隐藏,用户看不到任何验证元素追求零打扰的页面,如表单提交后台静默验证

三种 Widget 模式对比

"Managed 模式是官方推荐的——它会根据每个访客的风险等级动态调整。大部分真人用户走无感通道,只有 Cloudflare 判定可疑的流量才会弹出复选框。而且那个复选框只是勾一下,不是选红绿灯。"

"国内类似的产品,比如极验的"智能无感验证",思路和 Turnstile 的 Managed 模式很像——先无感判断,可疑了再降级到交互验证。原理上是相通的。"


实操指导

第一步:创建 Turnstile Widget

"理论讲完,上手操作。"老张打开了 Cloudflare Dashboard。

  1. 登录 Cloudflare Dashboard,左侧菜单选择 Turnstile
  2. 点击 Add Widget(添加 Widget)
  3. 填写配置:
    • Widget namespark-ai-register(给注册接口用的)
    • Domainspark-ai.pages.dev(你的域名,可添加多个)
    • Widget Mode:选择 Managed(推荐)
  4. 点击 Create

创建完成后,你会看到两个关键值:

Sitekey:    0x4AAAAAAAxxxxxxxxxxxxxxxx   (公开密钥,放在前端)
Secret Key: 0x4AAAAAAAxxxxxxxxxxxxxxxx (私密密钥,放在后端)

"Sitekey 是公开的,嵌在前端 HTML 里谁都看得到——这不影响安全。Secret Key 必须保密,只在后端 Worker 里使用。如果 Secret Key 泄露了,攻击者可以自己伪造验证请求,Turnstile 就形同虚设了。"

"如果你用的是 API 或 Terraform 管理,也可以通过 API 创建 Widget。但刚开始建议用 Dashboard,操作更直观。"

第二步:前端集成——嵌入 Turnstile 组件

"前端集成有两种方式:隐式渲染和显式渲染。先讲最简单的隐式渲染。"

隐式渲染(适合简单页面)

"隐式渲染就是加一行 script 标签,再放一个 div——Turnstile 会自动扫描带 cf-turnstile class 的元素并渲染。"

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>星火AI - 注册</title>
<!-- 引入 Turnstile 脚本 -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
<form id="register-form" action="/api/users/register" method="POST">
<input type="text" name="username" placeholder="用户名" required />
<input type="email" name="email" placeholder="邮箱" required />
<input type="password" name="password" placeholder="密码" required />

<!-- Turnstile Widget -->
<div
class="cf-turnstile"
data-sitekey="0x4AAAAAAAxxxxxxxxxxxxxxxx"
data-theme="auto"
data-callback="onTurnstileSuccess"
></div>

<button type="submit" id="submit-btn" disabled>注册</button>
</form>

<script>
// Turnstile 验证成功后回调,token 会作为参数传入
function onTurnstileSuccess(token) {
console.log("Turnstile 验证通过,token:", token);
document.getElementById("submit-btn").disabled = false;
}
</script>
</body>
</html>

"有几个关键点:"

  1. 脚本地址必须是 https://challenges.cloudflare.com/turnstile/v0/api.js,不要代理或缓存这个文件——Turnstile 更新后你的代理副本会失效
  2. 当 widget 放在 <form> 内时,Turnstile 会自动创建一个名为 cf-turnstile-response 的隐藏 input,表单提交时 token 会随其他字段一起发送到后端
  3. data-callback 指向验证成功后的回调函数,参数是 token 字符串(最多 2048 个字符)
  4. 提交按钮默认 disabled,验证通过后才启用——防止用户在验证完成前提交

"如果你需要更精细的控制——比如在 SPA 里动态渲染、条件渲染——用显式渲染。"

显式渲染(适合 SPA / 动态页面)

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script>

<div id="turnstile-container"></div>

<script>
// 页面加载后程序化渲染 widget
window.onload = function () {
const widgetId = turnstile.render("#turnstile-container", {
sitekey: "0x4AAAAAAAxxxxxxxxxxxxxxxx",
theme: "auto",
callback: function (token) {
console.log("验证成功:", token);
// 把 token 存起来,提交时带上
window.turnstileToken = token;
},
"error-callback": function (errorCode) {
console.error("验证失败:", errorCode);
},
"expired-callback": function () {
console.warn("Token 已过期,请重新验证");
// token 5 分钟后过期,需要重置
turnstile.reset(widgetId);
},
});
};
</script>

显式渲染的 JavaScript API:

方法说明
turnstile.render(container, config)在指定容器渲染 widget,返回 widgetId
turnstile.reset(widgetId)重置 widget,清除当前状态(token 过期时用)
turnstile.getResponse(widgetId)获取当前 token
turnstile.remove(widgetId)移除 widget
turnstile.isExpired(widgetId)检查 token 是否已过期

"在星火AI 的场景里,注册页面是静态页面,用隐式渲染就够了。如果你后面做 SPA 版本的管理后台,再切显式渲染。"

第三步:后端验证 Token——在 Worker 中调用 siteverify

"前端验证通过只是第一步——后端必须验证 token。"老张加重了语气,"前端的东西都可以被绕过,攻击者完全可以直接调你的 API。后端验证才是真正的安检门。"

前后端验证闭环与 Token 生命周期

Turnstile 的后端验证流程:

前端 → 提交表单(含 token) → Worker → 调用 siteverify → Cloudflare 返回验证结果 → 放行或拒绝

验证端点:

POST https://challenges.cloudflare.com/turnstile/v0/siteverify

"在 Worker 里实现验证:"

// src/turnstile.js

const TURNSTILE_SECRET = "0x4AAAAAAAxxxxxxxxxxxxxxxx"; // 你的 Secret Key

/**
* 验证 Turnstile token
* @param {string} token - 前端提交的 cf-turnstile-response
* @param {string} remoteip - 访客 IP(可选)
* @returns {Promise<{success: boolean, errors?: string[]}>}
*/
async function verifyTurnstileToken(token, remoteip) {
if (!token) {
return { success: false, errors: ["missing-input-response"] };
}

const formData = new FormData();
formData.append("secret", TURNSTILE_SECRET);
formData.append("response", token);
if (remoteip) {
formData.append("remoteip", remoteip);
}

try {
const response = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
body: formData,
}
);

const result = await response.json();

return {
success: result.success,
errors: result["error-codes"] || [],
challenge_ts: result.challenge_ts, // 验证时间戳
hostname: result.hostname, // 验证来源域名
action: result.action, // 自定义动作标识(可选)
};
} catch (error) {
console.error("Turnstile 验证请求失败:", error);
return { success: false, errors: ["internal-error"] };
}
}

export { verifyTurnstileToken };

"然后在注册接口里调用验证:"

// src/index.js

import { verifyTurnstileToken } from "./turnstile.js";

export default {
async fetch(request, env) {
const url = new URL(request.url);

// ========== 注册接口 ==========
if (url.pathname === "/api/users/register" && request.method === "POST") {
const body = await request.formData();

// 1. 取出前端提交的 Turnstile token
const turnstileToken = body.get("cf-turnstile-response");

// 2. 获取访客 IP(Cloudflare 自动注入的请求头)
const clientIP =
request.headers.get("CF-Connecting-IP") ||
request.headers.get("X-Forwarded-For") ||
"unknown";

// 3. 验证 token —— 这是关键一步!
const verification = await verifyTurnstileToken(turnstileToken, clientIP);

if (!verification.success) {
return Response.json(
{
error: "人机验证失败,请重试",
code: "TURNSTILE_FAILED",
// 开发阶段可以返回具体错误,生产环境不要暴露
detail: verification.errors,
},
{ status: 403 }
);
}

// 4. 验证通过,继续处理注册逻辑
const username = body.get("username");
const email = body.get("email");
const passwordHash = body.get("password_hash"); // 前端应先做哈希

try {
const result = await env.SPARK_DB
.prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)")
.bind(username, email, passwordHash)
.run();

return Response.json({
success: true,
userId: result.meta.last_row_id,
});
} catch (e) {
// 唯一约束冲突(用户名或邮箱已存在)
if (e.message.includes("UNIQUE")) {
return Response.json(
{ error: "用户名或邮箱已被注册" },
{ status: 409 }
);
}
throw e;
}
}

return Response.json({ error: "Not Found" }, { status: 404 });
},
};

"有几个细节必须注意:"

Token 的生命周期

特性
有效期300 秒(5 分钟)
使用次数1 次(用完即废)
过期/重复使用返回 timeout-or-duplicate 错误

"用户验证通过后如果 5 分钟内没提交表单,token 就过期了。这时候需要前端调用 turnstile.reset(widgetId) 重新生成 token。这也是为什么前面显式渲染的代码里要监听 expired-callback。"

siteverify 返回值

{
"success": true,
"challenge_ts": "2025-07-11T10:30:00.096Z",
"hostname": "spark-ai.pages.dev",
"error-codes": [],
"action": "register",
"cdata": "custom-data"
}
字段说明
success验证是否成功
challenge_ts验证完成时间(ISO 8601)
hostname验证发生时的域名(可用于校验来源)
error-codes错误码数组(验证失败时才有)
action自定义动作标识(前端通过 data-action 设置)
cdata自定义数据(前端通过 data-cdata 设置)

"进阶用法:你可以在前端设置 data-action="register",后端验证时检查 result.action === "register"——这样即使 token 被拿到别的接口重放,也能被识别出来。"

常见错误码

错误码含义处理方式
missing-input-secret没传 Secret Key检查后端代码
invalid-input-secretSecret Key 无效去 Dashboard 核对密钥
missing-input-response没传 token前端检查 widget 是否正常渲染
invalid-input-responsetoken 无效或过期提示用户刷新页面重试
timeout-or-duplicatetoken 过期或已被使用重置 widget 重新验证
internal-errorCloudflare 内部错误建议重试

第四步:用测试密钥调试

"开发阶段你不想每次测试都触发真实验证——尤其是跑自动化测试的时候。"老张说,"Turnstile 提供了一组测试密钥:"

测试 Sitekey行为测试 Secret Key验证结果
1x00000000000000000000AA总是通过(可见)1x0000000000000000000000000000000AA总是成功
2x00000000000000000000AB总是失败(可见)2x0000000000000000000000000000000AA总是失败
1x00000000000000000000BB总是通过(不可见)1x0000000000000000000000000000000AA总是成功
3x00000000000000000000FF强制交互验证

"用法很简单——开发时用测试 Sitekey 替换真实 Sitekey,测试 Secret Key 替换真实 Secret Key。测试密钥在 localhost 上也能用,不需要额外配置域名。"

// 根据环境自动切换密钥
const TURNSTILE_SITEKEY = env.ENVIRONMENT === "production"
? "0x4AAAAAAARealSiteKeyHere" // 生产环境
: "1x00000000000000000000AA"; // 开发环境(总是通过)

const TURNSTILE_SECRET = env.ENVIRONMENT === "production"
? "0x4AAAAAAARealSecretKeyHere" // 生产环境
: "1x0000000000000000000000000000000AA"; // 开发环境(总是通过)

"建议把密钥放在 Worker 的环境变量里,而不是硬编码在代码中——这样切换环境只需要改变量,不用改代码。在 wrangler.jsonc 里配置:

{
"vars": {
"ENVIRONMENT": "development",
"TURNSTILE_SITEKEY": "1x00000000000000000000AA",
"TURNSTILE_SECRET": "1x0000000000000000000000000000000AA"
}
}

生产环境用 wrangler secret put TURNSTILE_SECRET 设置真实密钥,不会写入代码仓库。"

自定义样式

"Turnstile 支持三种主题:light(亮色)、dark(暗色)、auto(自适应)。通过 data-theme 属性设置:

<!-- 暗色主题 -->
<div class="cf-turnstile" data-sitekey="..." data-theme="dark"></div>

<!-- 跟随系统(推荐) -->
<div class="cf-turnstile" data-sitekey="..." data-theme="auto"></div>

还有三种尺寸:normal(标准)、compact(紧凑)、flexible(弹性宽度,自适应容器):

<!-- 紧凑模式,适合移动端 -->
<div class="cf-turnstile" data-sitekey="..." data-size="compact"></div>

<!-- 弹性宽度,适合响应式布局 -->
<div class="cf-turnstile" data-sitekey="..." data-size="flexible"></div>

"星火AI 的注册页面用了 auto 主题 + normal 尺寸,在手机和电脑上都能正常显示。如果你的页面是暗色主题,选 darkauto 就行。"


小结预告

本篇知识点回顾

知识点核心内容
传统验证码问题reCAPTCHA 体验差(选红绿灯)、隐私问题严重(数据发给 Google 用于广告)
Turnstile 定位Cloudflare 的智能 CAPTCHA 替代方案,免费、无感、不依赖 CDN
Proof of Work给浏览器出计算难题(哈希前缀匹配),增加机器人批量请求的 CPU 成本
Proof of Space空间证明挑战,与 PoW 配合进一步增加自动化成本
浏览器指纹通过 Web API 探测、Canvas/WebGL 指纹、浏览器怪癖识别 Headless 浏览器和脚本
行为分析鼠标轨迹、点击位置、滚动行为、输入节奏等人类行为特征
Managed 模式根据风险等级自动决定:无感通过或弹出复选框(推荐)
Non-Interactive 模式显示加载动画但不需交互
Invisible 模式完全隐藏,用户看不到任何验证元素
Sitekey vs Secret KeySitekey 公开放前端,Secret Key 私密放后端
隐式渲染<script> + <div class="cf-turnstile">,自动扫描渲染
显式渲染turnstile.render() 程序化控制,适合 SPA
cf-turnstile-responsewidget 放在 form 内时自动创建的隐藏 input,携带 token
siteverify 端点POST https://challenges.cloudflare.com/turnstile/v0/siteverify
Token 有效期300 秒(5 分钟),单次使用,过期/重复返回 timeout-or-duplicate
后端验证必须前端验证可被绕过,后端 siteverify 才是真正的安检门
测试密钥1x...AA 总是通过,2x...AA 总是失败,3x...AA 模拟已用 token
主题与尺寸light/dark/auto 三种主题,normal/compact/flexible 三种尺寸
免费不限量Turnstile 完全免费,不限制调用次数

动手挑战

  1. 基础挑战:在 Cloudflare Dashboard 创建一个 Turnstile Widget,用隐式渲染把验证组件嵌入到注册页面。提交表单后在 Worker 中打印 token,确认前端到后端的传递链路通畅。

  2. 进阶挑战:完成完整的"前端验证 + 后端 siteverify"闭环。在 Worker 中实现 token 验证逻辑:验证通过才写 D1 数据库,验证失败返回 403。然后用测试密钥 2x00000000000000000000AB(总是失败)测试拒绝流程是否正常工作。

  3. 折腾挑战:给星火AI 的登录接口也加上 Turnstile 验证,但这次用 Invisible 模式——用户完全无感。在后端额外校验 result.hostname 是否匹配你的域名、result.action 是否为 "login"——防止 token 被跨接口重放。思考:如果攻击者拿到了一个有效的 login token,能否用它来调注册接口?加了 action 校验后呢?

提示:Token 是单次使用的——验证一次后就失效了。所以即使 token 被截获,攻击者也只能用一次,而且必须在 5 分钟内、从相同域名提交。再加上 action 校验,安全性就更高了。不过,如果攻击者能在 5 分钟内截获并重放 token,说明你的站点可能已经被 XSS 了——那是另一层安全问题,后面讲 WAF 时会涉及。

下回预告

林一把 Turnstile 装到注册接口上,机器人流量瞬间清零——没有有效 token 的请求被 Worker 直接返回 403,D1 数据库终于清静了。

"张哥,验证码搞定了,机器人进不来了。"林一满意地靠在椅背上。

老张点了点头,但表情并不轻松:"机器人挡住了,但你想过没有——如果有人不通过注册接口,直接对着你的 API 发恶意请求呢?SQL 注入、XSS 攻击、路径穿越……这些可不是验证码能挡的。"

"你是说……黑客?"

"别说那么吓人,但确实是这么回事。你的 Worker 暴露在公网上,任何人都能访问。光防机器人不够,还得防恶意请求。"

"那怎么防?"

"Cloudflare 的 WAF——Web Application Firewall。给你的 API 加一道防火墙,自动拦截常见攻击。"

下篇预告:《光防机器人不够,还得防黑客——WAF 防火墙》

wp