《林一的 Cloudflare 通关记》第 10 篇-挡住那些"机器人"——Turnstile 验证码替代方案
《林一的 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 张券,算题的时间成本就不可忽视了。"

"除了 PoW,Turnstile 还会使用 Proof of Space(空间证明)等其他挑战类型,进一步增加自动化成本。"
浏览器指纹与环境探测
"第二个机制是浏览器指纹。Turnstile 会在浏览器里跑一系列 JavaScript 探测,收集环境信号:"
| 探测维度 | 具体内容 | 能识别什么 |
|---|---|---|
| Web API 存在性 | navigator.webdriver、window.chrome 等 | Headless 浏览器、自动化工具 |
| Canvas 指纹 | 绘制特定图形,读取像素数据 | 虚拟机、模拟器环境 |
| WebGL 指纹 | GPU 渲染特征 | 无头浏览器缺少 GPU 加速 |
| 屏幕特征 | 分辨率、色深、像素比 | 批量克隆的虚拟机 |
| 时区与语言 | Intl.DateTimeFormat | IP 地理位置与浏览器设置是否一致 |
| 浏览器怪癖 | 特定浏览器才有的行为差异 | 伪造 User-Agent 的脚本 |
"真实浏览器的环境非常复杂——不同操作系统、不同浏览器版本、不同显卡,会产生细微但独特的指纹。机器人要么用 Headless 浏览器(指纹特征和真实浏览器不一样),要么用 HTTP 库直接发请求(根本没有浏览器环境),在指纹探测面前很容易暴露。"
"比如 navigator.webdriver 这个属性,在 Selenium、Puppeteer 驱动的浏览器中默认是 true,而正常用户浏览器里是 undefined 或 false。虽然高级机器人会尝试伪造,但 Turnstile 的探测不止这一项——它是一个信号矩阵,单项可以伪造,全部伪造且保持一致性很难。"

行为分析
"第三个机制是行为分析。Turnstile 会在页面加载后悄悄记录用户的交互行为:"
- 鼠标移动轨迹(是否平滑、是否有自然的加速减速)
- 点击位置分布(真实用户的点击位置有随机偏移,脚本通常精准点击中心)
- 滚动行为(是否有阅读节奏)
- 页面停留时间(是秒级提交还是毫秒级提交)
- 键盘输入节奏(打字速度是否有波动)
"一个真人打开注册页面,通常先看两眼页面,移动鼠标到输入框,打字有快有慢,偶尔打错再删掉。而机器人呢?页面刚加载完,0.1 秒内填完所有字段,鼠标轨迹是直线——甚至没有鼠标轨迹,直接程序化提交。"
"Turnstile 把这三类信号——PoW、浏览器指纹、行为分析——综合起来打分。风险低的访客直接通过,什么都不用做;风险稍高的可能需要勾选一个复选框(Managed 模式);风险极高的会被直接拒绝。"

"所以 Turnstile 的全称叫'智能 CAPTCHA 替代方案'——它不是没有验证,而是把验证从'用户做题'变成了'后台分析'。大多数真人用户完全无感,只有可疑流量才会被拦截或要求额外交互。"
Turnstile vs reCAPTCHA:详细对比
老张在白板上画了张对比表:
| 对比维度 | reCAPTCHA v2/v3 | Cloudflare 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 | 完全隐藏,用户看不到任何验证元素 | 追求零打扰的页面,如表单提交后台静默验证 |

"Managed 模式是官方推荐的——它会根据每个访客的风险等级动态调整。大部分真人用户走无感通道,只有 Cloudflare 判定可疑的流量才会弹出复选框。而且那个复选框只是勾一下,不是选红绿灯。"
"国内类似的产品,比如极验的"智能无感验证",思路和 Turnstile 的 Managed 模式很像——先无感判断,可疑了再降级到交互验证。原理上是相通的。"
实操指导
第一步:创建 Turnstile Widget
"理论讲完,上手操作。"老张打开了 Cloudflare Dashboard。
- 登录 Cloudflare Dashboard,左侧菜单选择 Turnstile
- 点击 Add Widget(添加 Widget)
- 填写配置:
- Widget name:
spark-ai-register(给注册接口用的) - Domain:
spark-ai.pages.dev(你的域名,可添加多个) - Widget Mode:选择 Managed(推荐)
- Widget name:
- 点击 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>
"有几个关键点:"
- 脚本地址必须是
https://challenges.cloudflare.com/turnstile/v0/api.js,不要代理或缓存这个文件——Turnstile 更新后你的代理副本会失效 - 当 widget 放在
<form>内时,Turnstile 会自动创建一个名为cf-turnstile-response的隐藏 input,表单提交时 token 会随其他字段一起发送到后端 data-callback指向验证成功后的回调函数,参数是 token 字符串(最多 2048 个字符)- 提交按钮默认
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。后端验证才是真正的安检门。"

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-secret | Secret Key 无效 | 去 Dashboard 核对密钥 |
missing-input-response | 没传 token | 前端检查 widget 是否正常渲染 |
invalid-input-response | token 无效或过期 | 提示用户刷新页面重试 |
timeout-or-duplicate | token 过期或已被使用 | 重置 widget 重新验证 |
internal-error | Cloudflare 内部错误 | 建议重试 |
第四步:用测试密钥调试
"开发阶段你不想每次测试都触发真实验证——尤其是跑自动化测试的时候。"老张说,"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 尺寸,在手机和电脑上都能正常显示。如果你的页面是暗色主题,选 dark 或 auto 就行。"
小结预告
本篇知识点回顾
| 知识点 | 核心内容 |
|---|---|
| 传统验证码问题 | 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 Key | Sitekey 公开放前端,Secret Key 私密放后端 |
| 隐式渲染 | <script> + <div class="cf-turnstile">,自动扫描渲染 |
| 显式渲染 | turnstile.render() 程序化控制,适合 SPA |
| cf-turnstile-response | widget 放在 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 完全免费,不限制调用次数 |
动手挑战
-
基础挑战:在 Cloudflare Dashboard 创建一个 Turnstile Widget,用隐式渲染把验证组件嵌入到注册页面。提交表单后在 Worker 中打印 token,确认前端到后端的传递链路通畅。
-
进阶挑战:完成完整的"前端验证 + 后端 siteverify"闭环。在 Worker 中实现 token 验证逻辑:验证通过才写 D1 数据库,验证失败返回 403。然后用测试密钥
2x00000000000000000000AB(总是失败)测试拒绝流程是否正常工作。 -
折腾挑战:给星火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 防火墙》
