Explainers

Cloudflare Turnstile 小部件模式:托管、非交互式、不可见

打开一个用了 Cloudflare Turnstile 的登录页,你可能什么都看不到——页面加载完了,没有复选框,也没有验证码图片。这不代表验证码没生效,而是 Turnstile 正用三种模式之一悄悄工作:托管(Cloudflare 自己判断)、非交互式(只跑工作量证明)、不可见(容器都不出现在视口里)。三种模式最终都产出同一个 cf-turnstile-response token,差别只在页面上看不看得出当前是哪一种。


三步判断当前是哪种模式

打开开发者工具,按顺序排查:

  1. 有没有复选框或加载动画?完全没提示,才可能是不可见模式。
  2. 查看 data-appearance,值是 interaction-only 就是非交互模式。
  3. 查看 data-size,值是 invisible 或容器不在视口里,才是不可见模式;都不满足就是托管模式。

三种模式速查表

排查不出结论时,对着下表核对差异:

特征 托管 非交互式 不可见
小部件可见? 有时 从不(仅一个加载动画) 从不
需要容器元素? 需要 需要 需要(但隐藏)
需要用户交互? 有时(点击复选框) 不需要 不需要
会跑工作量证明挑战? 会(可能升级为更难的挑战) 会(始终会) 会(始终会)
失败时会退回复选框? 不会(直接失败) 不会(直接失败)
token 输出字段 cf-turnstile-response cf-turnstile-response cf-turnstile-response
CaptchaAI 对应方法 turnstile turnstile turnstile
常见使用场景 登录、注册 低摩擦表单 后台校验

托管模式:默认配置,行为最不固定

托管模式把决定权交给 Cloudflare:多数用户无声通过,可疑流量看到复选框,风险很高的流量可能面对更复杂挑战。习惯极验(GeeTest)滑块交互的读者可能觉得意外——Turnstile 默认反而尽量不打扰用户。

实现方式

<!-- Managed mode (default) -->
<div class="cf-turnstile"
     data-sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg"
     data-theme="light">
</div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

自动化脚本怎么判断托管模式

托管模式根据请求方的信号动态调整:

  • 高信任度:无感通过,没有可见的 UI
  • 中等信任度:弹出复选框小部件,需要点击验证
  • 低信任度:触发交互式挑战,甚至直接拦截

托管模式最常见也最多变,不能提前假设小部件可不可见;用下面这个函数从 HTML 里做个粗判断:

def is_managed_mode(html):
    """Check if Turnstile is using managed mode (default)."""
    # Managed mode is the default — no explicit mode attribute
    has_turnstile = "cf-turnstile" in html
    has_explicit_mode = 'data-appearance="interaction-only"' in html or \
                        'data-appearance="always"' in html or \
                        'appearance: "interaction-only"' in html
    return has_turnstile and not has_explicit_mode

非交互模式:只跑工作量证明,界面上不会出现交互控件

非交互模式从不显示复选框或可点击元素。它在后台跑工作量证明挑战,页面上最多能看到加载动画;没法以非交互方式完成时会直接失败,不会像托管模式那样升级。

实现方式(HTML 属性或 JavaScript API 均可)

<!-- Non-interactive mode -->
<div class="cf-turnstile"
     data-sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg"
     data-appearance="interaction-only">
</div>
turnstile.render('#turnstile-container', {
    sitekey: '0x4AAAAAAAC3DHQhMMQ_Rxrg',
    appearance: 'interaction-only',
    callback: function(token) {
        document.getElementById('cf-turnstile-response').value = token;
    },
});

执行流程

Page loads → Widget initializes
    ↓
Background proof-of-work runs
    ↓
Success → Token generated (no visible UI)
    OR
Failure → Widget reports error (no fallback to checkbox)

什么场景会用非交互模式

评论区、反馈小部件、邮件订阅,或任何要把摩擦降到最低的场景都常用这个模式,已有浏览器端保护、只需再加一层验证的 API 端点也是如此。


不可见模式:页面上完全找不到容器

不可见模式才是真正的"看不见"——视口里不会出现容器元素。小部件在页面加载或被代码触发时运行,全程没有任何视觉提示。

实现方式(HTML 属性或纯 JavaScript 触发)

<!-- Invisible mode — container is hidden -->
<div id="turnstile-invisible"
     class="cf-turnstile"
     data-sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg"
     data-size="invisible">
</div>
// Programmatic invisible Turnstile
turnstile.render('#hidden-container', {
    sitekey: '0x4AAAAAAAC3DHQhMMQ_Rxrg',
    size: 'invisible',
    callback: function(token) {
        // Token ready — submit form automatically
        submitForm(token);
    },
    'error-callback': function() {
        // Challenge failed
        console.error('Invisible Turnstile failed');
    },
});

不可见模式的容器没有任何可见尺寸,单纯看渲染结果很难判断它存不存在,这也是它最难排查的地方:

import re

def detect_invisible_turnstile(html):
    """Detect invisible Turnstile on a page."""
    indicators = {
        "script_loaded": "challenges.cloudflare.com/turnstile" in html,
        "size_invisible": 'data-size="invisible"' in html or
                          "size: 'invisible'" in html or
                          'size: "invisible"' in html,
        "api_render_call": "turnstile.render" in html,
        "response_field": "cf-turnstile-response" in html,
    }

    if indicators["script_loaded"] and indicators["size_invisible"]:
        return {"mode": "invisible", "confidence": "high"}
    elif indicators["script_loaded"] and indicators["api_render_call"]:
        return {"mode": "invisible_or_programmatic", "confidence": "medium"}
    elif indicators["response_field"]:
        return {"mode": "turnstile_present", "confidence": "low"}

    return {"mode": "none", "confidence": "high"}

识别模式时最容易踩的坑

症状 原因 处理方式
token 有效,表单还是拒绝 sitekey 用错了(和可见小部件不一致) 检查 JS 渲染出来的 sitekey
HTML 里找不到小部件 不可见模式渲染后才加载 等页面加载完,查 XHR 响应
页面有多个 Turnstile 小部件 不同表单各配了不同 sitekey 把 sitekey 和表单对应起来
data-size="compact" 干扰判断 compact 只是尺寸变体,不是模式 compact 默认走托管模式
页面有 data-action 属性 分析用的标签,不是模式 如需校验,把 action 带上即可
token 提交前就过期 token 约 300 秒后失效 拿到就立刻提交,别插入等待

三种模式,CaptchaAI 走的是同一套 API

不管页面渲染的是托管、非交互还是不可见,CaptchaAI 这边的解决方式一样——提交 sitekey 和 pageurl,method 固定是 turnstile

出海团队做登录页 QA 常踩这个坑:staging 本地看到的往往是托管模式复选框,生产环境面向海外访客却切到不可见模式,只覆盖前者的脚本一上线就两眼一抹黑。

先拿到 sitekey 再说

无论页面用哪种模式,解决验证码都离不开 sitekey,下面这段代码可以从任意模式里提取它:

import re

def extract_turnstile_sitekey(html):
    """Extract Turnstile sitekey from page HTML (works for all modes)."""

    # Pattern 1: data-sitekey attribute in HTML
    match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', html)
    if match:
        return match.group(1)

    # Pattern 2: JavaScript render call
    match = re.search(r"sitekey:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", html)
    if match:
        return match.group(1)

    # Pattern 3: Turnstile config object
    match = re.search(r"siteKey['\"]?\s*[:=]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", html)
    if match:
        return match.group(1)

    return None

Python

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_turnstile(sitekey, page_url):
    """Solve any Turnstile mode — managed, non-interactive, or invisible."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "turnstile",
        "sitekey": sitekey,
        "pageurl": page_url,
        "json": 1,
    })

    task_id = submit.json()["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()

        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("Turnstile solve timed out")


# Use with any mode
token = solve_turnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://staging.example.com/qa-login")
print(f"Token: {token[:50]}...")

Node.js

const axios = require("axios");

const API_KEY = "YOUR_API_KEY";

async function solveTurnstile(sitekey, pageUrl) {
  const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
    params: {
      key: API_KEY,
      method: "turnstile",
      sitekey,
      pageurl: pageUrl,
      json: 1,
    },
  });

  const taskId = submit.data.request;

  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));

    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: taskId, json: 1 },
    });

    if (result.data.status === 1) {
      return result.data.request;
    }
  }

  throw new Error("Turnstile solve timed out");
}

// Same function works for all Turnstile modes
solveTurnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://staging.example.com/qa-login")
  .then((token) => console.log("Token:", token.substring(0, 50)));

常见问题

下面几个问题是实际接入时问得最多的。

Turnstile 用哪种模式,会影响 CaptchaAI 的解决结果吗?

不会。三种模式在 CaptchaAI 都是同一个 turnstile method,只需要 sitekey 和 pageurl。

为什么同样是托管模式,有的用户看到复选框,有的直接就过了?

Cloudflare 按信任度动态调整:高信任度无感通过,中等信任度弹出复选框,可疑流量可能触发更复杂挑战——都在同一个 turnstile method 内部,CaptchaAI 侧不用区分。

同一个网站会不会在不同页面切换模式?

会。不少站点默认走托管模式,特定页面或用户分组会切成非交互模式,sitekey 通常不变,每次导航重新判断更稳妥。

Turnstile 的 token 大概什么时候会过期?

约 300 秒后失效,拿到就立刻提交,别插入等待步骤。

从国内环境调用 CaptchaAI 解决 Turnstile,网络上会有障碍吗?

提交和轮询走的是 CaptchaAI 自己的接口(ocr.captchaai.com),跟用户能不能打开目标网站是两回事——拿到 sitekey 和 pageurl 即可出结果。


总结

三种模式——托管、非交互式、不可见——决定的只是用户能看到什么,最终都产出同一个 cf-turnstile-response token。用 CaptchaAI 的 Turnstile 解决方案 走同一套调用逻辑即可;真正的差别在识别环节:托管模式在 HTML 里就能看出特征,不可见模式则需要更深入的页面分析才能挖出 sitekey。

相关文章

该文章已禁用评论。