合集

林一的 Cloudflare 通关记

已更新 15 章
阅读进度 67%

第 10 章

第 10 篇-挡住那些"机器人"——Turnstile 验证码替代方案

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

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

故事引入

plaintext
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 给浏览器出一道计算难题——通常是找到一个特定哈希前缀的随机数。浏览器需要不断尝试不同的输入值,计算哈希,直到找到满足条件的解。”

老张画了个示意流程:

plaintext
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.webdriver、window.chrome 等Headless 浏览器、自动化工具
Canvas 指纹绘制特定图形,读取像素数据虚拟机、模拟器环境
WebGL 指纹GPU 渲染特征无头浏览器缺少 GPU 加速
屏幕特征分辨率、色深、像素比批量克隆的虚拟机
时区与语言Intl.DateTimeFormatIP 地理位置与浏览器设置是否一致
浏览器怪癖特定浏览器才有的行为差异伪造 User-Agent 的脚本

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

“比如 navigator.webdriver 这个属性,在 Selenium、Puppeteer 驱动的浏览器中默认是 true,而正常用户浏览器里是 undefined 或 false。虽然高级机器人会尝试伪造,但 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 name:spark-ai-register(给注册接口用的)
    • Domain:spark-ai.pages.dev(你的域名,可添加多个)
    • Widget Mode:选择 Managed(推荐)
  4. 点击 Create

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

plaintext
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 的元素并渲染。”

html
<!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 / 动态页面)

html
<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 的后端验证流程:

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

验证端点:

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

“在 Worker 里实现验证:”

javascript
// 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 };

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

javascript
// 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 返回值

json
{
  "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 上也能用,不需要额外配置域名。”

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

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

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

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

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

自定义样式

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

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

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

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

html
<!-- 紧凑模式,适合移动端 -->
<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 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 防火墙》