打开一个用了 Cloudflare Turnstile 的登录页,你可能什么都看不到——页面加载完了,没有复选框,也没有验证码图片。这不代表验证码没生效,而是 Turnstile 正用三种模式之一悄悄工作:托管(Cloudflare 自己判断)、非交互式(只跑工作量证明)、不可见(容器都不出现在视口里)。三种模式最终都产出同一个 cf-turnstile-response token,差别只在页面上看不看得出当前是哪一种。
三步判断当前是哪种模式
打开开发者工具,按顺序排查:
- 有没有复选框或加载动画?完全没提示,才可能是不可见模式。
- 查看
data-appearance,值是interaction-only就是非交互模式。 - 查看
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。