📚 离职知识库

SOP:Cloudflare 人机验证机制 + 给自己的网站上防爬

沉淀日期:2026-08-03 起因:看到 grok.com 的「正在进行安全验证」页面,想搞清楚这是什么、以及怎么给自己的站点用上 适用范围:个人开发者,站点多为 Next.js on Vercel(域名 *.saveme505.help,DNS 在 vercel-dns)+ 少量 Cloudflare Workers/Pages + tky VPS 反代


#第一部分:这东西到底是什么(概念辨析)

#1.1 四个容易混为一谈的产品

名称 本质 触发者 长什么样 需要 Cloudflare 代理域名吗
Turnstile 可嵌入任意网站的 CAPTCHA 替代组件(widget),开发者用 sitekey 主动集成 网站自己的前端代码 页面局部一个小框:转圈 / 复选框 / 完全不可见 不需要
Challenge Pages(挑战页) Cloudflare 在反向代理层拦截整个 HTTP 请求,返回全屏 HTML 拦截页,过关才放行到源站 WAF 自定义规则 / Bot Management / Rate Limiting 的 action 选了 Managed Challenge 全屏白页,带站点域名大标题,「正在验证您是否是真人」→「验证成功。正在等待 xxx 响应」 需要(橙云代理)
Bot Fight Mode / Super Bot Fight Mode 域名级自动机器人打分 + 挑战,一键开关不写规则 Cloudflare 仪表盘开关 触发时同样弹 Challenge Page 需要
Security Level / Under Attack Mode 全局风险阈值旋钮;Under Attack 是极端临时档,几乎给所有访客发 JS 挑战 域名 Overview 页 Quick Action 也走 Challenge Page 那套 UI 需要

#1.2 三张截图的对应关系(本次调研的直接结论)

  • 转圈「正在验证...」 → 非交互阶段。浏览器后台在跑 JS(环境采集、proof-of-work、PAT 校验),用户无需操作,几秒自动过。
  • 复选框「请验证您是真人」 → 交互挑战。自动信号不够确定时,升级要求一次人工点击。
  • 全屏「正在验证您是否是真人…验证成功。正在等待 grok.com 响应」Challenge Page,不是 Turnstile

判别口诀:Turnstile 只是页面里的一小块,不会占满全屏,也永远不会说「正在等待 xxx 响应」——这句话本身就是代理层拦截的特征(请求还没到源站)。

为什么同一个站三种形态都能见到:判定是动态分级的。信号干净 → 静默转圈放行;风险居中 → 升级成复选框;风险高(数据中心 IP、VPN 出口、指纹异常、无历史声誉)→ 直接全屏拦截。grok.com 被吐槽验证死循环,社区反馈集中在 IP 信誉、浏览器扩展干扰、Cookie 异常,是 xAI 侧策略调得严,不是 Cloudflare 的锅。

#1.3 判定原理(从轻到重的四层)

  1. Private Access Token (PAT) —— iOS 16+/macOS Ventura+ 由系统级(Apple/运营商)匿名证明「这是真实设备」,不采集 PII,秒过。
  2. 浏览器环境 / TLS 指纹 —— Canvas、WebGL、UA、字体、时区一致性 + TLS 握手指纹 JA3/JA4(密码套件顺序、扩展顺序、GREASE 值)。Python requests 这类库的 JA3 早在黑名单里。
  3. Proof-of-Work —— 让浏览器跑一段哈希计算,正常几毫秒~几秒,难度随风险分动态调整(机房 IP 会拿到更难的题)。
  4. 行为信号 —— 鼠标轨迹、焦点、滚动、点击节奏喂给 ML 模型。第三方分析估算只对约 5% 的疑难流量触发。

#1.4 关键 Cookie(排障时要认得)

Cookie 作用 有效期
cf_clearance 「已通过挑战」的凭证,签发一次后免重复验证。与生成时的 TLS 指纹 + IP + UA 绑定。属性 SameSite=None; Secure; Partitioned 由 Challenge Passage 配置,业界观察默认约 30 分钟
__cf_bm Bot Management 的持续机器人评分/会话标识,几乎每次访问都刷新 30 分钟无操作后过期
__cfuvid Rate Limiting 用于区分共享 IP 背后的不同用户
__cflb 负载均衡会话保持 几秒 ~ 24 小时可配
cf_chl_rc_* 挑战平台内部诊断

二者区别:cf_clearance = 通行证(一次性签发),__cf_bm = 持续监控评分。排障时 cf_clearance 损坏/过期是「验证死循环」的头号嫌疑。

#1.5 2025-2026 新动态

  • Turnstile 早在 2023-09 就 GA,Managed 模式永久免费、官方未设请求量上限
  • Turnstile Analytics(2025-03):挑战次数、通过率、拦截率趋势。
  • Pre-Clearance:让 SPA 用 Turnstile 主动签发 cf_clearance,后续 API 调用不会被全屏挑战页打断。
  • Ephemeral ID(企业版):设备级跨会话异常关联,不依赖 IP。
  • AI Labyrinth(2025-03):给不守 robots.txt 的 AI 爬虫喂 AI 生成的假内容迷宫,消耗其资源,同时把「跟随了隐藏链接」当强信号回灌模型。静默处理,不提示对方。
  • Pay-per-crawl(2025,2025-12-10 增强):用 HTTP 402 对 AI 爬虫按请求收费,可设 Allow / Charge / Block。
  • Content Signals Policy(2025-09):robots.txt 扩展语法,分别声明 search / ai-input / ai-train 三种细分授权,已推广 380 万+ 域名,但纯自愿、无强制力
  • 2025-07-01 起,Cloudflare 新建站点的 robots.txt 默认对 AI 训练类爬虫设为 Block

#第二部分:给自己的站点上防爬(决策与落地)

#2.1 先做决策:走哪条路

最关键的前提事实

Cloudflare 的 Managed Challenge / WAF / Bot Fight Mode 必须让流量先经过 Cloudflare 边缘,也就是要求域名的权威 NS 在 Cloudflare(Full Setup)。 只把子域名 CNAME 指过去的 Partial/CNAME Setup 仅 Business/Enterprise 可用,Free 计划用不了。

Turnstile 完全不依赖 DNS 托管在哪——它是纯前端 widget + 后端 siteverify API 调用,站在 Vercel 上照样能用。

据此分两档:

#【轻量档】不动 DNS —— 推荐作为默认选择

适用:绝大多数 *.saveme505.help 子站(DNS 在 vercel-dns,且已有 HMAC 密码门)

  1. 关键接口挂 Turnstile(Invisible 或 Managed 模式):登录接口、注册/表单提交、任何会写库或花钱的 API
  2. 接口级限速:Upstash Redis / Vercel KV,例如登录接口每 IP 每分钟 5 次
  3. 需要时开 Vercel Firewall 的 Attack Challenge Mode(所有计划免费,仅在检测到异常流量时才对全站弹挑战);Vercel BotID 基础版免费,Deep Analysis(Kasada 驱动)需付费并在 Firewall → Rules 手动开启
  4. 公开静态站维持公开,只用 robots.txt + Content Signals 声明 AI 训练授权

#【重装档】迁 NS 到 Cloudflare + 橙云代理

适用:tky VPS 反代的服务、流量敏感的 API 服务,或确实需要边缘层拦截的站点

  1. 域名 NS 迁到 Cloudflare(Full Setup,Free 计划可用)
  2. Vercel 侧保留自定义域名配置,Cloudflare 里对应记录开橙云
  3. Security → WAF → Custom rules 建规则,Action 选 Managed Challenge(官方推荐,优于 Interactive/JS Challenge)
  4. 对 webhook / 健康检查 / RSS 等路径显式加 Skip 规则

#2.2 套餐能力边界(别买错也别指望免费版)

能力 Free Pro Business Enterprise
Turnstile ✅ 免费无限
WAF Custom Rules + Managed Challenge ✅(条数有限,一般 5 条)
Bot Fight Mode
Super Bot Fight Mode ✅(Definitely Automated) ✅(+ Likely Automated)
Bot Score cf.bot_management.score 字段
完整 Bot Management(自定义阈值/逐请求打分) ✅(加购)
CNAME/Partial Setup(不迁 NS)

#2.3 ⚠️ Free 版 Bot Fight Mode 的大坑

Bot Fight Mode 是域名级一刀切,不能按路径豁免,且明确不能被 Skip 规则跳过(官方文档写死了)。这意味着一旦开启,你的 webhook、支付回调、健康检查、RSS 全都可能被拦。

结论:Free 计划下,宁可自己写 WAF Custom Rule + Managed Challenge(可精确控制路径、可 Skip),也别图省事开 Bot Fight Mode。


#第三部分:Turnstile 在 Next.js App Router 的接法

#3.1 机制要点

  • sitekey 前端公开;secret key 服务端私密,绝对不能加 NEXT_PUBLIC_ 前缀
  • 前端拿 token → 传给自己后端 → 后端调 POST https://challenges.cloudflare.com/turnstile/v0/siteverifyapplication/x-www-form-urlencoded
  • token 有效期 300 秒,且只能被验证一次。重复验证返回 timeout-or-duplicate
  • 三种模式:Managed(默认,风险高才弹交互)/ Non-Interactive(可见但不用点)/ Invisible(完全不可见)。表单场景推荐 Invisible 或 Managed

#3.2 环境变量

# .env.local
NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAA...   # 公开
TURNSTILE_SECRET_KEY=0x4AAA...             # 服务端私密,切勿加 NEXT_PUBLIC_ 前缀

#3.3 客户端组件

// components/turnstile-widget.tsx
'use client';
import { Turnstile } from '@marsidev/react-turnstile'; // 社区封装,比手写 script 标签方便

export function TurnstileWidget({ onVerify }: { onVerify: (token: string) => void }) {
  return (
    <Turnstile
      siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
      options={{ theme: 'auto', size: 'flexible' }}
      onSuccess={onVerify}
    />
  );
}

#3.4 服务端校验(Route Handler)

// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';

async function verifyTurnstile(token: string, ip: string | null) {
  const formData = new URLSearchParams();
  formData.append('secret', process.env.TURNSTILE_SECRET_KEY!);
  formData.append('response', token);
  if (ip) formData.append('remoteip', ip);
  // idempotency_key 可选:防止网络超时重试被误判为「token 重复使用」
  formData.append('idempotency_key', crypto.randomUUID());

  const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    body: formData,
  });
  return res.json() as Promise<{ success: boolean; 'error-codes'?: string[] }>;
}

export async function POST(req: NextRequest) {
  const body = await req.json();
  const { turnstileToken, ...formFields } = body;

  const ip = req.headers.get('x-forwarded-for')?.split(',')[0] ?? null;
  const verification = await verifyTurnstile(turnstileToken, ip);

  if (!verification.success) {
    return NextResponse.json({ error: '人机验证失败' }, { status: 403 });
  }

  // 关键:verify 成功后立即处理业务逻辑,不要把 token 存起来复用
  return NextResponse.json({ ok: true });
}

#3.5 siteverify 错误码速查

错误码 含义
missing-input-secret / invalid-input-secret secret key 没传或不对
missing-input-response / invalid-input-response token 没传或不合法
bad-request 请求格式错误
timeout-or-duplicate token 已过期(>300s)或已被验证过一次 —— 最常见
internal-error Cloudflare 侧内部错误,可重试

idempotency_key 陷阱:同一 token + 同一 key 重试是安全的;但不同 token 复用同一个 key,会拿到缓存的旧结果

#3.6 避免 token 复用的三条纪律

  1. 一次表单提交只用一次 token;提交成功或失败后都在 onExpire/onError 里 reset 组件重新挑战
  2. 服务端不要缓存已通过的 token 当「已验证白名单」
  3. 高价值接口(支付、注册)让 token 与业务数据在同一个 HTTP 请求体里一起提交,避免「先验证拿票据、再异步提交业务」的两段式设计被重放

#3.7 middleware 的取舍

middleware 里只做「是否携带有效 token/cookie」的粗筛和限速;真正的 siteverify 网络调用放 Route Handler——Edge Runtime 里 fetch 外部 API 有延迟和冷启动成本。


#第四部分:攻防现状(防守视角的威胁建模)

对手绕过 Cloudflare/Turnstile 的六种主流手法:

  1. Headless 特征规避 —— undetected-chromedriver、Playwright/Puppeteer + stealth 抹除 navigator.webdriver、CDP 痕迹、Canvas/WebGL 指纹
  2. TLS 指纹伪装 —— curl-impersonate / curl_cffiimpersonate="chrome")精确复刻真实浏览器的 JA3/JA4 握手
  3. 住宅代理 / 蜂窝代理 —— 提升 IP 信誉分,规避机房 IP 的高风险判定
  4. 第三方打码服务 —— 2Captcha / Anti-Captcha / CapSolver 提交挑战参数,10-30 秒返回已解出的 token,成本约 1000 次 $1.45
  5. 倒卖 cf_clearance cookie —— 灰产维护已解出的 cookie 池,配合稳定的 IP+UA+TLS 组合复用到过期
  6. 注意curl-impersonate 不执行 JS,遇到真正需要跑脚本的挑战仍得回退到真实浏览器/云端浏览器集群——这是 JS 挑战的价值所在

#防守方结论:不要依赖单点

「验证通过 = 一定是人」这个假设是错的。 打码服务能稳定拿到合法 token,验证码只能筛掉「懒惰的脚本」,筛不掉「愿意花钱雇人/AI 解码」的对手。

必须叠加:

  • 三层纵深:DNS 层(WAF/Managed Challenge)+ 应用层(Turnstile 挂关键端点)+ 业务层(限速、签名、异常检测)
  • 速率异常检测优先于验证码:同一 IP/Cookie/指纹短时高频命中同一接口,即使 token 验证通过也要限速
  • 蜜罐字段 + 时间陷阱:不可见的 honeypot input(正常用户不会填)+「提交耗时 < 1-2 秒视为可疑」
  • 高价值端点用签名/密钥而非验证码(见下节)

#第五部分:误伤与排障

#5.1 ⛔ 绝对不能加验证/挑战的路径

这些都是服务器对服务器调用,不具备执行 JS 的能力,加了等于业务静默失败:

  • Webhook / 支付回调(Stripe、微信支付回调等)
  • RSS / Atom feed(面向阅读器和聚合器)
  • 已鉴权用户的 API(携带有效 session/JWT 的请求应基于 token 合法性放行)
  • 健康检查 / uptime 监控端点/api/health、UptimeRobot 探活)——被拦会造成误报下线告警

做法:在 WAF Custom Rules 或 Vercel Firewall 里对这些路径显式加 Skip/放行规则,不要指望系统「聪明」识别。

#5.2 常见误伤群体

  • 国内用户 / 机场代理 / 云服务器 IP —— IP 信誉分低,容易触发交互挑战甚至循环
  • VPN 用户 —— 社区确认大量「无限循环」案例关掉 VPN 立刻解决
  • 隐私浏览器 / 严格隐私扩展 / 广告拦截器 —— 会拦截或改写挑战所需 JS,导致脚本执行不完整卡死
  • Cookie 异常 —— cf_clearance 损坏或过期导致反复重来
  • 无障碍用户 —— Turnstile 宣称 WCAG 2.2 AA 合规,但社区仍反馈屏幕阅读器焦点陷阱、失败无语音反馈(未确认是否已修复,建议实测)

#5.3 「用户卡在验证循环」排查四步

  1. 清 Cookie/缓存,换隐身窗口重试
  2. 临时关闭 VPN/代理测试
  3. 检查 Cloudflare SSL/TLS 模式是否为 Full / Full Strict —— 排除「看起来像验证循环、实际是 HTTPS 重定向死循环」的情况
  4. 到 Security Events 用该访客的 cf-ray 或 IP 查具体被哪条规则拦了,确认不是自己规则写错(比如全站 Challenge 而非仅敏感路径)

#5.4 ⚠️ Vercel + Cloudflare 橙云的两个已知坑

坑一:ERR_TOO_MANY_REDIRECTS

  • 原因:Cloudflare SSL/TLS 模式设成 Flexible 时,浏览器→CF 是 HTTPS,但 CF→Vercel 源站发 HTTP;Vercel 强制 HTTPS 又把请求重定向回去,死循环。
  • 解决:SSL/TLS 模式改成 Full 或 Full Strict

坑二:证书卡在 "Failed to Generate Cert"

  • 原因:Vercel 走 ACME 校验签发证书时,域名已开橙云,Cloudflare 会拦截或改写校验请求。
  • 解决:先在灰云(仅 DNS)状态让 Vercel 签发好证书,确认域名验证通过后再切橙云
  • 泛域名证书场景下,代理模式与 _acme-challenge 标签可能冲突,按 Cloudflare 官方 Partial Setup 指引处理。

#第六部分:监控与验证

#6.1 看哪里

  1. Cloudflare Security Events:Dashboard → Security → Events,每条请求被哪个产品拦截/挑战/标记,含触发规则、IP、UA、URI、国家
  2. Security → Analytics → Bot analysis:机器人流量占比
  3. Turnstile Analytics:Dashboard → Turnstile,每个 site key 的挑战次数/通过率/拦截率趋势
  4. Vercel Firewall 面板:Attack Challenge Mode 触发记录、BotID 拦截统计

#6.2 关键日志字段

字段 说明
cf-ray 唯一请求 ID,让用户提供这个可精确定位那次请求
cf.bot_management.score 1-99,越低越像机器人(需 Pro/Business 起步
cf.bot_management.verified_bot 是否 Cloudflare 认证的良性爬虫(Googlebot 等)

付费计划可通过 Log Explorer / Logpush 导出到自建 SIEM。

#6.3 自测方法

# 1. curl 裸打受保护路径 —— 应被拦截(返回挑战页或 403),而不是 200
curl -I https://your-domain.com/protected-path

# 2. 确认放行路径没被误伤
curl -I https://your-domain.com/api/health   # 应该 200
  1. 无头浏览器基线对比:先用无 stealth 的 Playwright 打一发(「裸机器人」基线),再用带 stealth 的打一发,对比两者结果,评估防护能挡住哪个层级的对手。
  2. 检查 Route Handler 日志里 siteverifysuccess 字段在正常用户请求里符合预期,确认没误伤自己的流量。

#第七部分:针对我自己站点的落地清单

按当前架构(Vercel + vercel-dns + HMAC 密码门)的推荐动作:

  • 静态展示站 / 导航站:不折腾,维持公开。只补 robots.txt + Content Signals 声明 AI 训练授权
  • 有登录/表单的动态站:现有 HMAC 密码门已是很强的第一层(未登录进不了内容页)。在此之上:
    • 登录接口 /api/login 加 Turnstile(Invisible 模式,不打扰)
    • 登录接口加 Upstash Redis 限速:每 IP 每分钟 5 次
    • 任何写库/花钱的 API(生图、LLM 调用)加同样两层
  • 不需要迁 DNS——上面这些都不依赖 Cloudflare 代理
  • 真要边缘防护的服务(tky VPS 反代、敏感 API):单独把该子域名 NS 迁 Cloudflare + 橙云 + WAF Custom Rules,严格按「先灰云验证书 → 再切橙云」的顺序
  • 无论哪种方案:支付回调、健康检查、RSS 务必显式排除在验证规则之外
  • 别开 Free 版 Bot Fight Mode(一刀切且不可 Skip)

#附:官方文档索引

Cloudflare 概念与配置

Vercel 侧

Next.js 集成示例

攻防参考(防守方威胁建模用)

社区排障帖

来源:沉淀/SOP-Cloudflare人机验证与网站防爬.md(整理于 2026-08-18)