站点返回的是 503 还是 403?这一个状态码就够告诉你,现在面对的是托管挑战还是互动挑战。托管挑战由 Cloudflare 按访客风险自适应决定难度,互动挑战则不管风险高低一律显示可见验证码。分清楚两者,才能选对 CaptchaAI 的求解方法。
托管挑战:Cloudflare 怎么决定谁该被拦
托管挑战是 Cloudflare 推荐的默认动作,先跑一遍风险评估,再挑一个打扰最小的挑战级别:
判定逻辑
WAF rule triggers Managed Challenge
↓
Cloudflare evaluates visitor signals:
├─ Browser fingerprint quality
├─ IP reputation score
├─ TLS fingerprint (JA3/JA4)
├─ Request history
├─ Behavioral signals
└─ Device capabilities
↓
Risk assessment → Challenge level selected:
├─ LOW risk → Invisible pass (no visible UI)
├─ MEDIUM risk → Non-interactive Turnstile (background PoW)
├─ HIGH risk → Interactive Turnstile (checkbox/widget)
└─ VERY HIGH risk → JavaScript challenge page (5s wait)
↓
Challenge completed
↓
qa_session_cookie cookie issued
不同访客遇到的实际体验也不一样:
- 常规浏览器,IP 信誉良好(约 90%)——什么都看不到,无感通过。
- 新浏览器,IP 中性(约 5%)——短暂的加载动画。
- 有可疑信号(约 4%)——弹出 Turnstile 复选框。
- 高风险信号(约 1%)——整页显示"正在检查你的浏览器……"提示。
需要展示界面时,用的是 Cloudflare 自己的挑战平台:
<!-- Managed Challenge page (when visible) -->
<body>
<div id="challenge-stage">
<div id="challenge-body-text">
Verifying you are human. This may take a few seconds.
</div>
<!-- Turnstile widget (when rendered) -->
<div class="cf-turnstile"
data-sitekey="0x4AAAAAAAC3DHQhMMQ_Rxrg">
</div>
</div>
<!-- Challenge platform script -->
<script src="/cdn-cgi/challenge-platform/h/g/orchestrate/managed/v1?ray=...">
</script>
</body>
互动挑战:老规则,谁都躲不过
互动挑战是更早的旧规则,只要命中就一定展示必须手动完成的验证码——没有无感通过这回事。
请求流程
WAF rule triggers Interactive Challenge
↓
Full-page CAPTCHA served (HTTP 403)
↓
Visitor must interact with CAPTCHA widget
↓
CAPTCHA solved
↓
qa_session_cookie cookie issued
每个访客都会看到一个整页挑战,界面和 Turnstile 类似的小部件:
<!-- Interactive Challenge page -->
<body>
<div id="challenge-running">
<div class="main-wrapper">
<h2>Please verify you are human</h2>
<!-- Always-visible challenge widget -->
<div class="challenge-widget">
<!-- Checkbox + verification -->
</div>
</div>
</div>
</body>
Cloudflare 不建议继续用它,原因很直接:
- 始终可见——100% 的访客都会看到验证码。
- 摩擦更高——每个访客都必须手动交互。
- 跳出率更高——用户更容易直接离开,而不是完成验证。
- 不做风险区分——就算是常客也照样被拦。
- 可访问性差——每个人都得手动完成交互,无一例外。
一张表看懂:托管挑战 vs 互动挑战
一个容易被忽略的细节:这两种挑战都是 Cloudflare 自己的技术栈,不依赖 Google 的 reCAPTCHA 脚本,不会出现验证码图标在国内网络下转圈转不出来的情况——这也是不少跨境电商团队更愿意先排查 Cloudflare 挑战、再排查 reCAPTCHA 的原因。
| 对比项 | 托管挑战 | 互动挑战 |
|---|---|---|
| 上线时间 | 2021 年 | 更早的旧版规则(2021 年之前) |
| 是否自适应 | 是,Cloudflare 按访客风险决定难度 | 否,一律要求互动 |
| 能不能无感通过 | 能,约 90% 的访客完全无感 | 不能,验证码始终可见 |
| 挑战形式 | 无感 → Turnstile → JS 挑战,逐级升级 | 验证码始终可见 |
| Cloudflare 是否推荐 | 是,新规则默认用它 | 否,仅为兼容旧规则保留 |
| 用户摩擦 | 低,大多数人什么都看不到 | 高,每次都要手动交互 |
| HTTP 状态码 | 503 | 403 |
| CaptchaAI 方法 | turnstile 或 cloudflare_challenge |
turnstile |
站点从互动迁移到托管,脚本要跟着改什么
Cloudflare 建议所有站点从互动挑战迁移到托管挑战。目标站点赶上这次迁移时,需要关注:
- 互动 → 托管:有约 90% 的概率开始无感通过。
- HTTP 403 → 503:状态码判断逻辑要跟着更新。
- 始终可见 → 自适应:小部件不一定会出现在 HTML 里。
- sitekey 不变:CaptchaAI 的求解代码不用改。
- qa_session_cookie 输出不变:cookie 处理逻辑不用改。
怎么判断当前面对的是哪种挑战
最可靠的办法是直接读 HTTP 状态码和响应体特征。下面这段脚本自动区分托管挑战、互动挑战、IUAM(旧版 JS 挑战)和纯 WAF 拦截四种情况:
import requests
import re
def identify_challenge_type(url):
"""Determine if a URL uses Managed or Interactive Challenge."""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
"Accept": "text/html,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
response = requests.get(url, headers=headers, timeout=15, allow_redirects=False)
html = response.text
status = response.status_code
result = {
"url": url,
"status": status,
"challenge_type": None,
"cf_ray": response.headers.get("cf-ray", ""),
"solve_method": None,
}
if status == 200:
# Check for inline Turnstile widget (not a challenge page)
if "cf-turnstile" in html:
result["challenge_type"] = "turnstile_widget"
result["solve_method"] = "turnstile"
else:
result["challenge_type"] = "none"
return result
if status == 503:
# 503 indicates Managed Challenge or IUAM
if "managed" in html or "challenge-platform" in html:
result["challenge_type"] = "managed_challenge"
result["solve_method"] = "turnstile" # Managed renders as Turnstile
elif "jschl" in html:
result["challenge_type"] = "iuam_js_challenge"
result["solve_method"] = "cloudflare_challenge"
else:
result["challenge_type"] = "unknown_503"
return result
if status == 403:
if "challenge" in html.lower():
result["challenge_type"] = "interactive_challenge"
result["solve_method"] = "turnstile"
else:
result["challenge_type"] = "waf_block"
result["solve_method"] = None # Hard block, not solvable
return result
return result
# Usage
info = identify_challenge_type("https://protected-site.com/login")
print(f"Challenge: {info['challenge_type']}")
print(f"Solve with: {info['solve_method']}")
判定特征表
| 特征 | 托管挑战 | 互动挑战 | IUAM | WAF 直接拦截 |
|---|---|---|---|---|
| HTTP 状态码 | 503 | 403 | 503 | 403 |
HTML 中含 challenge-platform |
✅ | ✅ | ❌ | ❌ |
脚本路径含 managed |
✅ | ❌ | ❌ | ❌ |
HTML 中含 jschl |
❌ | ❌ | ✅ | ❌ |
| 始终显示小部件 | ❌ | ✅ | ❌ | ❌ |
| 存在无感通过的可能 | ✅ | ❌ | ❌ | ❌ |
用 CaptchaAI 分别解决两种挑战
判断出挑战类型之后,接下来就是选对 method 参数——两种挑战最终都提交到同一组 in.php / res.php 接口,差别只在下面这一个字段。
解决托管挑战
需要交互时通常渲染成 Turnstile 小部件,用 CaptchaAI 的 turnstile 方法处理:
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_managed_challenge(url, sitekey=None):
"""Solve Cloudflare Managed Challenge."""
# If sitekey not provided, extract from page
if not sitekey:
import re
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
}
page = requests.get(url, headers=headers, timeout=15)
match = re.search(
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', page.text
)
sitekey = match.group(1) if match else None
if not sitekey:
# No visible Turnstile — try cloudflare_challenge method
return solve_js_challenge(url)
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": 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("Solve timed out")
def solve_js_challenge(url):
"""Fallback to cloudflare_challenge method."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "cloudflare_challenge",
"sitekey": "managed",
"pageurl": 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("Solve timed out")
解决互动挑战
互动挑战永远显示可见的小部件,用的是同一个 turnstile 方法:
def solve_interactive_challenge(url, sitekey):
"""Solve Cloudflare Interactive Challenge (legacy)."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": 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("Solve timed out")
Node.js(两种挑战通用)
Node.js 脚本可以用下面这段代码同时覆盖两种类型,靠 type 参数切换:
const axios = require("axios");
const API_KEY = "YOUR_API_KEY";
async function solveCloudflareChallenge(url, type = "managed") {
const method = type === "js_challenge" ? "cloudflare_challenge" : "turnstile";
const sitekey =
type === "js_challenge" ? "managed" : await extractSitekey(url);
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method,
sitekey: sitekey || "managed",
pageurl: url,
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("Solve timed out");
}
async function extractSitekey(url) {
try {
const response = await axios.get(url, {
headers: { "User-Agent": "Mozilla/5.0 Chrome/120.0.0.0" },
validateStatus: () => true,
});
const match = response.data.match(
/data-sitekey=["']([0-9x][A-Za-z0-9_-]+)["']/
);
return match ? match[1] : null;
} catch {
return null;
}
}
排查清单
- 先看页面返回的是整页挑战,还是嵌在业务流程里的小部件,再确认有没有
cf-chl标记、Turnstile 容器或对应回调钩子。 - 把页面 URL 和代理出口信息一起记录下来,方便定位是不是同一个目标反复换了策略。
常见问题现象
| 症状 | 原因 | 处理方式 |
|---|---|---|
| 503,看不到任何小部件 | 托管挑战无感通过了 | 不用处理,你已经通过了 |
| 503,页面写着"正在检查你的浏览器" | 托管挑战升级成了 JS 挑战 | 改用 cloudflare_challenge 方法 |
| 403,有可见验证码 | 命中的是互动挑战(旧版) | 用 turnstile 方法 |
| 403,但没有任何挑战内容 | WAF 直接拦截,不是挑战 | 换 IP 或调整请求节奏 |
| 挑战类型时不时会变 | 托管挑战在按信号动态调整 | Turnstile 和 JS 挑战都要覆盖到 |
| 一种类型下发的 cookie,另一种类型不认 | 两条挑战流程各自独立,域名相同 | 遇到哪种挑战就解哪种 |
常见问题
为什么同一个站点,这次是 503,下次却变成了 403?
大概率不是同一条 WAF 规则。一个站点可以对 /login 用托管挑战、对 /api/ 用互动挑战。看到状态码切换,先确认是不是命中了不同规则,而不是急着改代码。
只用 turnstile 方法,能不能同时搞定托管挑战和互动挑战?
大多数情况下可以,两者最常见的可见形式都是 Turnstile 小部件。只有当托管挑战升级成完整的 JS 挑战页面(503 加"正在检查你的浏览器")时,才需要切换到 cloudflare_challenge。
互动挑战会被 Cloudflare 官方下线吗?
目前没有正式弃用公告,但 Cloudflare 已明确建议所有新规则改用托管挑战,互动挑战只为兼容旧配置保留,站点随时可能自行迁移。
托管挑战和互动挑战,能用同一个 CaptchaAI 账号处理吗?
可以。两者共用同一个 API Key,区别只在于 method 参数,计费也不区分挑战类型,按并发线程计算,一个线程解完就能接下一个任务。
小结
托管挑战按访客风险自适应选择难度,从完全无感到整页 JS 挑战;互动挑战一律显示可见验证码。两者都会产生同样的 qa_session_cookie cookie,用 CaptchaAI 处理时,大多数场景用 turnstile 方法即可,遇到完整 JS 挑战页面再切换到 cloudflare_challenge。托管挑战是现在的默认做法,互动挑战只是为兼容旧规则保留。