深入理解 Cloudflare 过盾原理:无需 AI,从零实现 CF Turnstile 绕过
本文深入分析 CF-Gateway-Pro 的核心实现原理,从 Cloudflare Turnstile 的检测机制讲起,逐步拆解”反检测→点击验证→Cookie 复用→智能降级”的完整技术链路。读完你可以自己从零写一个。
如果你对 AI 驱动的通用验证码求解方案感兴趣,推荐阅读姊妹篇:《用 AI 从零实现通用验证码求解服务:OhMyCaptcha 原理剖析》
深入理解 Cloudflare 过盾原理:无需 AI,从零实现 CF Turnstile 绕过
Cloudflare Turnstile 到底在检测什么?
在写代码之前,我们必须先搞清楚对手。Turnstile 和传统验证码(reCAPTCHA 的”请选择红绿灯”)完全不同,它几乎不依赖视觉挑战,而是靠检测你的浏览器环境来判断你是不是机器人。
Turnstile 的检测维度主要包括:
1. 自动化工具特征
// Cloudflare 会检测这些属性
navigator.webdriver // Selenium/Playwright 会设为 true
window.cdc_adoQpoasnfa76pfcZLmcfl_Array // ChromeDriver 特有属性
window.__playwright // Playwright 特有属性
navigator.plugins.length // 自动化浏览器通常为 0普通 Chrome 打开一个页面,navigator.webdriver 是 undefined。但如果你用 Selenium 或 Playwright 控制浏览器,这个值会自动变成 true——这就是最简单的检测点。
2. TLS 指纹
每个 HTTP 客户端在建立 TLS 连接时,都会发送一个 Client Hello 消息。这个消息里包含了支持的密码套件、扩展列表、椭圆曲线等信息。不同的客户端(Chrome、Firefox、Python requests、curl)产生的 TLS 指纹完全不同。
真实 Chrome 136 的 JA3 指纹: 771,4865-4866-4867-49195-49199-49196-49200-...
Python requests 的 JA3 指纹: 771,49196-49200-159-52393-52392-52394-49195-...Cloudflare 会将你的 TLS 指纹和请求头中的 User-Agent 进行交叉验证——如果你 UA 声称是 Chrome,但 TLS 指纹是 Python,直接拒绝。
3. Canvas / WebGL 指纹
浏览器渲染同一个图形,不同硬件/驱动/系统环境产生的像素级差异是固定的。Cloudflare 用这个来:
- 区分真实浏览器和 headless 浏览器
- 跨会话追踪同一个用户
4. 行为特征
- 页面加载后多久出现交互
- 鼠标移动轨迹
- 网络请求时序
理解了这些,我们就知道要从哪些方面”伪装”。
核心原理一:反检测脚本注入
这是过盾的第一步,也是最关键的一步。你需要在页面 JavaScript 执行之前,先注入一段脚本来”修补”浏览器的自动化特征。
隐藏 navigator.webdriver
// 必须在页面加载之前注入
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
configurable: true
});
// 还要清理原型链上的痕迹
delete navigator.__proto__.webdriver;这是最重要的一行。没有这个,基本上所有 CF 盾都过不了。
伪造 navigator.plugins
自动化浏览器的 navigator.plugins 通常是空的,但真实 Chrome 至少有 3 个内置插件:
const pluginData = [
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
{ name: 'Native Client', filename: 'internal-nacl-plugin' }
];
// 构造一个完整的 PluginArray 对象
const pluginArray = {
length: pluginData.length,
item: function(index) { return this[index] || null; },
namedItem: function(name) { /* ... */ },
refresh: function() {}
};
pluginData.forEach((p, i) => { pluginArray[i] = { ...p }; });
Object.defineProperty(navigator, 'plugins', {
get: () => pluginArray,
configurable: true
});修复 window.chrome
// 真实 Chrome 有 window.chrome.runtime 对象
// 自动化浏览器可能没有或不完整
if (!window.chrome) {
window.chrome = {};
}
if (!window.chrome.runtime) {
window.chrome.runtime = {
connect: function() {},
sendMessage: function() {},
onMessage: { addListener: function() {} },
PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', ... },
// ... 完整的属性模拟
};
}清除 ChromeDriver 特征
// ChromeDriver 会在 window 上挂载 cdc_ 开头的属性
const cdcProps = Object.keys(window).filter(
k => k.startsWith('cdc_') || k.startsWith('$cdc_')
);
cdcProps.forEach(prop => delete window[prop]);防止 toString 检测
这是一个很狡猾的检测手段——检测方会调用你重写的函数的 toString(),如果返回的不是 [native code],就知道被篡改了:
const nativeToString = Function.prototype.toString;
const customFunctions = new WeakSet();
Function.prototype.toString = function() {
if (customFunctions.has(this)) {
// 返回伪造的 native code 格式
return 'function ' + this.name + '() { [native code] }';
}
return nativeToString.call(this);
};
customFunctions.add(Function.prototype.toString);伪造网络连接信息
Headless 浏览器可能没有 navigator.connection:
if (!navigator.connection) {
Object.defineProperty(navigator, 'connection', {
get: () => ({
effectiveType: '4g',
rtt: 50,
downlink: 10,
saveData: false
}),
configurable: true
});
}核心原理二:Canvas / WebGL 指纹随机化
直接修改 Canvas/WebGL 的全部输出会破坏页面渲染(包括 Turnstile 自己的动画)。关键技巧是只在指纹采集时注入噪声,正常渲染不干扰。
智能判断是否是指纹采集
function isFingerprinting(canvas) {
// 指纹检测通常使用小尺寸 Canvas(<= 300x300)
if (canvas.width <= 300 && canvas.height <= 300) {
return true;
}
// 检查是否有非空像素(说明有渲染内容,可能在做指纹)
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, 16, 16);
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i] !== 0) return true;
}
return false;
}只在 toDataURL 时加噪声
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(type, quality) {
if (isFingerprinting(this)) {
const ctx = this.getContext('2d');
const imageData = ctx.getImageData(0, 0, this.width, this.height);
// 每个像素加微小噪声(±1 以内)
for (let i = 0; i < imageData.data.length; i += 4) {
const noise = seededRandom(noiseSeed + i) * 2 - 1;
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise));
}
ctx.putImageData(imageData, 0, 0);
}
return originalToDataURL.call(this, type, quality);
};WebGL 渲染器伪造
// WebGL 参数 37446 = UNMASKED_RENDERER_WEBGL
// 检测方用这个区分真机和虚拟机/headless
const renderers = [
'ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Direct3D11 vs_5_0 ps_5_0)',
'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0)',
'ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)',
];
WebGLRenderingContext.prototype.getParameter = new Proxy(originalGetParameter, {
apply: function(target, thisArg, args) {
if (args[0] === 37446) {
return renderers[noiseSeed % renderers.length];
}
return Reflect.apply(target, thisArg, args);
}
});WebRTC 泄露防护
使用代理时,WebRTC 可能泄露真实 IP:
if (typeof RTCPeerConnection !== 'undefined') {
RTCPeerConnection.prototype.createDataChannel = function() { return null; };
RTCPeerConnection.prototype.createOffer = function() {
return Promise.reject('WebRTC disabled');
};
}核心原理三:Turnstile 验证码点击
反检测做好后,下一步是和 Turnstile 交互。这里选择 DrissionPage 而非 Playwright / Selenium 是有原因的——DrissionPage 底层基于 CDP (Chrome DevTools Protocol) 直接操作真实 Chrome,而不是像 Playwright 那样用修改过的 Chromium,TLS 指纹天然真实。
Turnstile 的 DOM 结构
Turnstile 的 checkbox 藏在多层 Shadow DOM 里:
<div> ← 外层容器
└── <input name="cf-turnstile-response"> ← 隐藏的 response 字段
└── #shadow-root
└── <iframe> ← Cloudflare 的 iframe
└── <body>
└── #shadow-root
└── <input> ← 真正的 checkbox!普通的 document.querySelector 无法穿透 Shadow DOM。DrissionPage 的 .shadow_root API 可以做到:
# 1. 找到 turnstile 响应字段
box = page.ele("@name=cf-turnstile-response", timeout=0.5)
# 2. 向上找到父容器,进入 shadow root
wrapper = box.parent()
iframe = wrapper.shadow_root.ele("tag:iframe")
# 3. 进入 iframe 的 shadow root,找到真正的 checkbox
cb = iframe.ele("tag:body").shadow_root.ele("tag:input")
# 4. 点击!
cb.click()完整的过盾循环
def solve_turnstile(url, proxy=None):
instance = browser_pool.acquire(timeout=60, proxy=proxy)
page = instance.page
page.get(url)
start_time = time.time()
click_count = 0
last_click_time = 0
while time.time() - start_time < 30: # 30秒超时
title = page.title.lower()
# 尝试找到并点击验证码
try:
box = page.ele("@name=cf-turnstile-response", timeout=0.5)
if box:
wrapper = box.parent()
iframe = wrapper.shadow_root.ele("tag:iframe")
cb = iframe.ele("tag:body").shadow_root.ele("tag:input")
# 避免频繁点击,至少间隔 1.5 秒
if cb and (time.time() - last_click_time) > 1.5:
click_count += 1
cb.click()
last_click_time = time.time()
except Exception:
pass
# 判断是否过盾成功:标题变了 + 验证码消失了
if "just a moment" not in title and "cloudflare" not in title:
# 双重确认:确保验证码元素也消失了
try:
still_has = page.ele("@name=cf-turnstile-response", timeout=0.3)
if still_has:
continue # 标题变了但验证码还在,继续等
except Exception:
pass # 找不到验证码元素 = 真的过盾了
time.sleep(1) # 等 cf_clearance Cookie 写入
break
# 提取凭证
cookies = parse_cookies(page.cookies())
ua = page.user_agent
return {"cookies": cookies, "ua": ua}关键细节:
- 多次点击支持:某些站点需要多次点击才能通过
- 点击间隔控制:至少 1.5 秒,避免被判定为机器人
- 双重成功判断:标题变化 + 验证码元素消失,防止误判
- 等待 Cookie:过盾成功后
cf_clearanceCookie 不是立即写入的,需要等 1 秒
核心原理四:Cookie 复用(性能关键)
每次都启动浏览器过盾太慢了(5-15 秒),而且资源消耗大。核心优化是——过一次盾,凭证复用 30 分钟。
凭证 = Cookie + User-Agent
过盾后返回两样东西:
cf_clearanceCookie:Cloudflare 的通行证User-Agent:必须和过盾时一致
# 凭证缓存结构
{
"cookies": {"cf_clearance": "xxx", "__cf_bm": "yyy", ...},
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
}为什么必须用 curl_cffi 而不是 requests?
这就回到 TLS 指纹的问题了。过盾时浏览器是 Chrome 136,Cloudflare 记录了对应的 TLS 指纹。如果你复用 Cookie 时用 Python requests 发请求,TLS 指纹变成了 Python 的,Cloudflare 发现指纹不匹配就会拒绝。
curl_cffi 可以模拟特定浏览器版本的 TLS 指纹:
from curl_cffi import requests as curl_requests
# impersonate="chrome136" 让 TLS 握手的行为完全模拟 Chrome 136
resp = curl_requests.get(
url,
cookies=cached_cookies,
headers={"User-Agent": cached_ua},
impersonate="chrome136"
)这样 Cloudflare 看到的 TLS 指纹和 Cookie 对应的浏览器一致 → 放行。
缓存的存储
# SQLite 存储(本地模式)
CREATE TABLE credentials (
domain TEXT PRIMARY KEY, -- 域名作为 key
cookies TEXT NOT NULL, -- JSON 序列化的 cookie dict
ua TEXT NOT NULL, -- User-Agent 字符串
expire_at REAL NOT NULL, -- 过期时间戳
created_at REAL NOT NULL
);
# 缓存查找逻辑
def get_credentials(url, force_refresh=False):
domain = urlparse(url).netloc
if not force_refresh:
row = db.query("SELECT * FROM credentials WHERE domain = ? AND expire_at > ?",
domain, time.time())
if row:
return {"cookies": json.loads(row.cookies), "ua": row.ua}
# 缓存未命中 → 启动浏览器过盾
creds = solve_turnstile(url)
db.upsert(domain, json.dumps(creds["cookies"]), creds["ua"],
time.time() + 1800) # 30分钟过期
return creds凭证自动刷新
后台看门狗每 5 分钟扫描一次,将即将过期(5 分钟内)的凭证提前刷新,避免请求时才发现 Cookie 失效:
async def watchdog_task():
while True:
await asyncio.sleep(300) # 每5分钟
# 找到即将过期的凭证
expiring = cache.get_expiring_domains(threshold_seconds=300)
for domain in expiring[:3]: # 每次最多刷新3个
cache.refresh_credential(domain)核心原理五:智能降级策略
不是所有站点 Cookie 复用都能成功。有些站点会做额外检测(比如检查请求时序、验证 Cookie 对应的 session 是否有完整的浏览记录)。CF-Gateway-Pro 实现了一套自动降级机制。
两种请求模式
CookieFetcher (默认) BrowserFetcher (备选)
│ │
├── 复用 Cookie + curl_cffi ├── 浏览器直接加载页面
├── 速度快 (0.5-2s) ├── 速度慢 (5-15s)
├── 资源消耗低 ├── 资源消耗高
└── 可能被拦截 └── 几乎不会被拦截拦截检测
def _is_response_blocked(resp):
# 1. 检查状态码
if resp.status_code in [403, 503, 429]:
return True
# 2. 检查 CF 专用响应头
if resp.headers.get("cf-mitigated") == "challenge":
return True
# 3. 检查页面内容特征(即使状态码是 200!)
check_text = resp.text[:10000]
blocked_patterns = [
"cf-turnstile", # Turnstile 组件
"challenge-platform", # CF 挑战平台
"_cf_chl_opt", # CF 挑战选项
"challenges.cloudflare.com/turnstile", # Turnstile 脚本 URL
]
for pattern in blocked_patterns:
if pattern in check_text:
return True
return False注意:即使状态码是 200,页面内容也可能是 CF 的验证页面,所以必须检查页面内容。
自动降级流程
def proxy_request(url, method="GET", headers=None, auto_fallback=True):
# 默认用 CookieFetcher
response = cookie_fetcher.fetch(url, method, headers)
# 检查是否被拦截
if auto_fallback and _is_response_blocked(response):
# 降级到 BrowserFetcher
response = browser_fetcher.fetch(url, method, headers)
return response域名智能学习
系统会记录每个域名在两种模式下的成功率,自动推荐最佳模式:
class DomainStats:
domain: str
cookie_success: int = 0
cookie_failure: int = 0
browser_success: int = 0
browser_failure: int = 0
def update_recommendation(self):
# Cookie 模式至少 5 次请求,且失败率 > 50%
if self.cookie_total >= 5 and self.cookie_failure_rate > 0.5:
self.recommended_mode = "browser"
# 如果 Cookie 失败率降到 25% 以下,恢复 Cookie 模式
elif self.recommended_mode == "browser":
if self.cookie_failure_rate <= 0.25:
self.recommended_mode = "cookie"统计数据 24 小时过期,定期重新评估。这意味着如果某个站点临时加强了检测,系统会暂时切换到 Browser 模式;恢复正常后又自动切回高效的 Cookie 模式。
核心原理六:浏览器池化
每次过盾都启动/关闭 Chrome 太昂贵了。CF-Gateway-Pro 实现了一个线程安全的浏览器对象池:
class BrowserPool:
def __init__(self, min_size=1, max_size=3, idle_timeout=300):
self._pool = Queue() # 可用实例队列
self._all_instances = [] # 所有实例列表
self._lock = threading.Lock() # 线程安全
def acquire(self, timeout=30, proxy=None):
"""借出一个浏览器实例"""
# 如果指定了代理,必须创建新实例(代理是启动参数)
if proxy:
return self._create_browser(proxy=proxy)
# 无代理:从池中获取
try:
instance = self._pool.get(timeout=timeout)
# 检查浏览器是否还活着
if not instance.page.process_id:
instance = self._create_browser()
instance.mark_used()
return instance
except Empty:
# 池空了,看能不能创建新的
if len(self._all_instances) < self.max_size:
return self._create_browser()
return None # 池满了
def release(self, instance):
"""归还浏览器实例"""
instance.mark_free()
self._pool.put(instance)
def destroy(self, instance):
"""销毁损坏的实例并补充新实例"""
instance.page.quit()
self._all_instances.remove(instance)
# 低于最小数量时自动补充
if len(self._all_instances) < self.min_size:
new = self._create_browser()
self._pool.put(new)关键设计:
- 代理实例不复用:因为代理是 Chrome 启动参数,不同代理需要不同实例
- 崩溃检测:通过
process_id判断浏览器进程是否还活着 - 损坏标记:过盾异常时标记实例为
_is_broken,归还时销毁而不是放回池 - 内存监控:看门狗定期检查每个浏览器进程的内存,超限自动重启
从零实现:最小可运行版本
理解了以上原理,你可以用不到 100 行代码写一个最简版本:
"""最小化 CF Turnstile 过盾 + Cookie 复用示例"""
import time
import json
from DrissionPage import ChromiumOptions, ChromiumPage
from curl_cffi import requests as curl_requests
# ========== 1. 反检测配置 ==========
STEALTH_JS = """
(function() {
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
delete navigator.__proto__.webdriver;
Object.defineProperty(navigator, 'plugins', {
get: () => [{name:'Chrome PDF Plugin'},{name:'Chrome PDF Viewer'},{name:'Native Client'}]
});
if (!window.chrome) window.chrome = {};
if (!window.chrome.runtime) window.chrome.runtime = {};
})();
"""
# ========== 2. 过盾核心 ==========
def solve(url):
co = ChromiumOptions()
co.set_argument("--disable-blink-features=AutomationControlled")
co.set_argument("--no-sandbox")
co.set_argument("--window-size=1920,1080")
co.headless(False)
page = ChromiumPage(co)
page.add_init_js(STEALTH_JS)
page.get(url)
start = time.time()
while time.time() - start < 30:
try:
box = page.ele("@name=cf-turnstile-response", timeout=0.5)
if box:
iframe = box.parent().shadow_root.ele("tag:iframe")
cb = iframe.ele("tag:body").shadow_root.ele("tag:input")
if cb:
cb.click()
time.sleep(2)
except Exception:
pass
if "just a moment" not in page.title.lower():
time.sleep(1)
break
cookies = {}
for c in page.cookies():
if isinstance(c, dict):
cookies[c["name"]] = c["value"]
ua = page.user_agent
page.quit()
return cookies, ua
# ========== 3. Cookie 复用 ==========
def fetch_with_cookies(url, cookies, ua):
return curl_requests.get(
url,
cookies=cookies,
headers={"User-Agent": ua},
impersonate="chrome136"
)
# ========== 使用 ==========
if __name__ == "__main__":
target = "https://某个CF保护的网站.com"
# 首次:浏览器过盾
cookies, ua = solve(target)
print(f"过盾成功!cf_clearance: {cookies.get('cf_clearance', 'N/A')}")
# 后续:直接用 Cookie
resp = fetch_with_cookies(target, cookies, ua)
print(f"状态码: {resp.status_code}, 内容长度: {len(resp.text)}")安装依赖:
pip install DrissionPage curl_cffi
# 还需要系统安装 Google Chrome整体架构总结
┌────────────────────────────────────┐
│ 客户端请求 │
└──────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────┐
│ ProxyService (调度层) │
│ 检查域名智能学习 → 选择模式 │
└──────┬───────────────┬─────────────┘
│ │
默认模式 │ 降级/推荐 │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ CookieFetcher │ │ BrowserFetcher │
│ │ │ │
│ 1.查缓存凭证 │ │ 1.从池获取浏览器 │
│ 2.curl_cffi发请求 │ │ 2.浏览器直接加载 │
│ 3.检测是否被拦截 │ │ 3.自动过盾 │
└────────┬─────────┘ │ 4.提取HTML │
│ └──────────────────┘
▼
┌──────────────────┐
│ CredentialCache │
│ (SQLite/Redis) │
│ │
│ 未命中? → Solver │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ solve_turnstile │
│ │
│ BrowserPool取实例 │
│ 注入反检测脚本 │
│ 点击Turnstile │
│ 提取Cookie+UA │
│ 归还实例到池 │
└──────────────────┘关键技术选型理由
| 选择 | 原因 |
|---|---|
| DrissionPage 而非 Playwright | 操作真实 Chrome,TLS 指纹天然真实 |
| curl_cffi 而非 requests | 支持 TLS 指纹模拟,Cookie 复用不会被识别 |
| SQLite 作为默认缓存 | 零外部依赖,单机足够 |
| 同步 API 而非异步 | DrissionPage 是同步库,但通过线程池 + FastAPI 实现并发 |
| headless=False | DrissionPage 在 False 模式下过盾率更高(配合 xvfb 在 Linux 上使用) |
⚠️ 免责声明:本文仅供安全研究和技术学习用途。请勿将相关技术用于未经授权的网站访问,遵守目标网站的服务条款和当地法律法规。