0x00 背景
通义千问(chat.qwen.ai)部署了阿里云 WAF(Web Application Firewall),对 API 请求进行多维检测。直接用 HTTP 客户端请求会被拦截,返回 403 或 WAF 拦截页面。
1 2 3 4 5 6
| import httpx
async with httpx.AsyncClient() as client: resp = await client.get("https://chat.qwen.ai/api/v1/auths/")
|
本文记录如何使用 Camoufox 反检测浏览器绕过阿里云 WAF,实现稳定的 API 调用。
0x01 阿里云 WAF 检测机制
阿里云 WAF 不是单一维度的检测,而是多信号联合判断。理解它的检测维度,才能有针对性地绕过。
检测维度
| 检测维度 |
说明 |
拦截依据 |
| TLS 指纹 |
检测 TLS 握手中的 Cipher Suite、扩展顺序等特征 |
Python httpx / requests 的 TLS 指纹与浏览器完全不同 |
| HTTP 头指纹 |
User-Agent、Accept-Language、sec-ch-ua 等头信息的一致性 |
缺少浏览器特有头或头信息组合异常 |
| 行为特征 |
请求频率、时间间隔、Cookie 变化模式 |
请求间隔过于均匀或过快 |
| JavaScript 执行 |
检测是否支持 JS 执行、Canvas 指纹等 |
纯 HTTP 客户端无法执行 JS 挑战 |
| Cookie 完整性 |
验证 Cookie 生成路径和签名 |
缺少 WAF 通过 JS 设置的验证 Cookie |
为什么传统方案失效
传统方案通常用 curl_cffi 或 tls-client 模拟 Chrome TLS 指纹:
1 2 3 4
| from curl_cffi.requests import AsyncSession
async with AsyncSession(impersonate="chrome124") as client: resp = await client.get("https://chat.qwen.ai/api/v1/auths/")
|
这种方式能解决 TLS 指纹问题,但解决不了:
- JS 挑战验证:WAF 可能下发 JS 挑战,纯 HTTP 客户端无法执行
- Cookie 生成链:某些 Cookie 是 WAF 通过 JS 动态生成并签名的,手动构造无法通过验证
- 行为模式:即使 TLS 和头信息都对了,请求时间模式仍然暴露自动化特征
0x02 Camoufox 简介
Camoufox 是基于 Firefox 的反检测无头浏览器,专为绕过浏览器指纹检测设计。
核心特性
| 特性 |
说明 |
| 指纹伪装 |
完全模拟真实 Firefox 浏览器指纹,包括 Canvas、WebGL、AudioContext |
| 人类化行为 |
自动添加随机延迟,模拟真实用户操作节奏 |
| 反自动化检测 |
隐藏 navigator.webdriver 等自动化标志 |
| 真实 TLS 指纹 |
使用 Firefox 真实 TLS 实现,指纹完全合法 |
| 字体指纹 |
根据配置的 OS 生成对应字体列表 |
与 Playwright / Selenium 的区别
| 特性 |
Camoufox |
Playwright + Firefox |
Selenium + Chrome |
navigator.webdriver |
隐藏 |
可被检测 |
可被检测 |
| TLS 指纹 |
真实 Firefox |
真实 Firefox |
真实 Chrome |
| Canvas 指纹 |
随机化 |
原始 |
原始 |
| 人类化延迟 |
内置 |
需手动实现 |
需手动实现 |
| 反检测能力 |
高 |
中 |
低 |
0x03 架构设计
整体采用混合引擎架构,结合浏览器引擎的稳定性和 HTTP 引擎的速度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ┌──────────────────────────────────────────────────────────────┐ │ qwen2API Gateway │ ├──────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ Browser │ │ HTTPX │ │ Hybrid │ │ │ │ Engine │ │ Engine │ │ Engine │ │ │ │ (Camoufox) │ │ (curl_cffi) │ │ (Browser+HTTPX) │ │ │ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │ │ │ │ │ │ │ └──────────────────┼───────────────────┘ │ │ ▼ │ │ ┌───────────────────────┐ │ │ │ chat.qwen.ai API │ │ │ │ (Aliyun WAF) │ │ │ └───────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
|
路由策略:
| 场景 |
首选引擎 |
回退引擎 |
| API 调用 (api_call) |
HTTPX(速度快) |
Browser(WAF 拦截时回退) |
| 流式请求 (fetch_chat) |
Browser(成功率高) |
HTTPX(浏览器失败时回退) |
0x04 Camoufox 配置与指纹伪装
浏览器配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| _CAMOUFOX_OPTS = { "headless": True, "humanize": True, "i_know_what_im_doing": True, "os": "windows", "locale": "zh-CN", "firefox_user_prefs": { "gfx.webrender.software": True, "media.hardware-video-decoding.enabled": False,
"browser.cache.disk.enable": True, "browser.cache.memory.enable": True,
"app.update.auto": False, "browser.shell.checkDefaultBrowser": False, }, }
|
关键参数解析
| 参数 |
值 |
反检测原理 |
humanize |
True |
自动添加随机延迟,避免请求时间模式被识别为机器人 |
os |
"windows" |
统一 OS 指纹,避免 TLS 指纹与 User-Agent 不一致 |
locale |
"zh-CN" |
模拟中国用户,与 chat.qwen.ai 目标用户群体匹配 |
browser.cache.* |
True |
真实用户浏览器有缓存行为,无缓存是自动化特征 |
gfx.webrender.software |
True |
服务器无 GPU,用软件渲染替代完全禁用(完全禁用是自动化特征) |
为什么 OS 指纹必须统一
这是一个容易被忽略的细节。TLS 握手过程中,客户端会发送 User-Agent 相关的扩展信息。如果配置了 os: "windows" 但 TLS 指纹显示是 Linux,WAF 就能识别出不一致。
Camoufox 的 os 参数不只是设置 navigator.platform,它会联动调整:
- TLS 指纹中的相关扩展
navigator.userAgent 字符串
- 字体列表(Windows 字体 vs Linux 字体)
- 屏幕分辨率和 DPI
0x05 页面池管理
设计思路
每次请求都创建新页面会带来两个问题:资源消耗大、指纹暴露多。页面池复用是更好的方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class BrowserEngine: def __init__(self, pool_size: int = 3, base_url: str = "https://chat.qwen.ai"): self.pool_size = pool_size self.base_url = base_url self._browser = None self._pages: asyncio.Queue = asyncio.Queue() self._ready = asyncio.Event()
async def _init_pages(self): """初始化页面池""" log.info(f"[Browser] 正在初始化 {self.pool_size} 个并发渲染引擎页面...") for i in range(self.pool_size): page = await self._browser.new_page()
await page.set_viewport_size({"width": 1920, "height": 1080})
await page.goto( self.base_url, wait_until="domcontentloaded", timeout=60000 )
await asyncio.sleep(0.5) self._pages.put_nowait(page) log.info(f" [Browser] Page {i+1}/{self.pool_size} ready")
|
关键设计
- 页面池复用:避免每次请求创建新页面,减少资源消耗和指纹暴露
- 预加载站点:提前访问 chat.qwen.ai,建立 Cookie 和会话状态
- 视口设置:1920x1080 模拟真实桌面浏览器分辨率
- 异步队列:用
asyncio.Queue 管理页面,天然支持并发获取和归还
页面刷新与恢复
当 JS 执行出错时,页面状态可能损坏。此时需要刷新页面恢复会话状态:
1 2 3 4 5 6 7 8 9 10 11 12
| async def _refresh_page(self, page): try: await asyncio.wait_for( page.goto(self.base_url, wait_until="domcontentloaded"), timeout=20000, ) except Exception: pass
async def _refresh_page_and_return(self, page): await self._refresh_page(page) self._pages.put_nowait(page)
|
在 api_call 中,根据执行结果决定是直接归还还是刷新后归还:
1 2 3 4
| if needs_refresh: asyncio.create_task(self._refresh_page_and_return(page)) else: self._pages.put_nowait(page)
|
注意刷新操作是异步的(create_task),不阻塞当前请求的返回。
0x06 JS 注入执行 Fetch
这是绕过 WAF 的核心手段。不在 Python 端直接发 HTTP 请求,而是在浏览器 JS 上下文中执行 fetch。
普通 API 调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| JS_FETCH = ( "async (args) => {" "const opts={" "method:args.method," "headers:{" "'Content-Type':'application/json'," "'Authorization':'Bearer '+args.token" "}" "};" "if(args.body)opts.body=JSON.stringify(args.body);" "const res=await fetch(args.url,opts);" "const text=await res.text();" "return{status:res.status,body:text};" "}" )
|
流式响应处理
SSE 流式响应的处理更复杂。由于 Camoufox 的 expose_function 跨语言回调有限制,采用在 JS 中完整收取流后一次性返回的方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| JS_STREAM_FULL = ( "async (args) => {" "const ctrl=new AbortController();" "const tmr=setTimeout(()=>ctrl.abort(),1800000);" "try{" "const res=await fetch(args.url,{" "method:'POST'," "headers:{" "'Content-Type':'application/json'," "'Authorization':'Bearer '+args.token" "}," "body:JSON.stringify(args.payload)," "signal:ctrl.signal" "});" "if(!res.ok){" "const t=await res.text();clearTimeout(tmr);" "return{status:res.status,body:t.substring(0,2000)};" "}" "const rdr=res.body.getReader();" "const dec=new TextDecoder();" "let body='';" "while(true){" "const{done,value}=await rdr.read();" "if(done)break;" "body+=dec.decode(value,{stream:true});" "}" "clearTimeout(tmr);" "return{status:res.status,body:body};" "}catch(e){" "clearTimeout(tmr);" "return{status:0,body:'JS error: '+e.message};" "}}" )
|
为什么在浏览器中执行 fetch
这是整个方案的关键决策,原因有四:
- Cookie 自动携带:浏览器自动管理 Cookie,无需手动处理 WAF 通过 JS 设置的验证 Cookie
- TLS 指纹一致:使用 Firefox 真实 TLS 实现,指纹完全合法,与页面访问时的 TLS 指纹一致
- JavaScript 环境完整:支持 WAF 的 JS 挑战验证,浏览器上下文中有完整的 DOM 和 JS 运行时
- 请求来源合法:从 chat.qwen.ai 域内发起 fetch,满足 Same-Origin 策略,Referer 和 Origin 头自然正确
0x07 请求抖动与防封控
请求抖动
均匀的请求间隔是自动化最明显的特征之一。添加随机抖动模拟真实用户:
1 2 3 4 5
| def _request_jitter_seconds() -> float: """生成随机请求延迟""" low = max(0, settings.REQUEST_JITTER_MIN_MS) high = max(low, settings.REQUEST_JITTER_MAX_MS) return random.uniform(low, high) / 1000.0
|
在每次 API 调用前添加抖动:
1
| await asyncio.sleep(_request_jitter_seconds())
|
账号最小间隔
单账号请求间隔过短会触发限流:
1 2 3 4
| min_interval = settings.ACCOUNT_MIN_INTERVAL_MS / 1000.0 wait_s = max(0.0, (acc.last_request_started + min_interval) - now) if wait_s > 0: await asyncio.sleep(wait_s)
|
限流冷却策略
被限流后采用指数退避,避免持续触发:
1 2 3 4 5 6 7 8 9
| def mark_rate_limited(self, acc, cooldown=None, error_message=""): acc.rate_limit_strikes += 1 base = cooldown or settings.RATE_LIMIT_BASE_COOLDOWN dynamic = min( settings.RATE_LIMIT_MAX_COOLDOWN, int(base * (2 ** max(0, acc.rate_limit_strikes - 1))) ) dynamic += int(_jitter_seconds()) acc.rate_limited_until = time.time() + dynamic
|
退避时间表:
| 触发次数 |
冷却时间 |
| 第 1 次 |
~600s |
| 第 2 次 |
~1200s |
| 第 3 次 |
~2400s |
| 第 N 次 |
~3600s(上限) |
并发控制
单账号最大并发设为 1,避免同一账号的多个请求同时到达 WAF:
0x08 混合引擎与 WAF 检测
WAF 拦截识别
WAF 拦截的响应有几种典型特征:
1 2 3 4 5 6 7 8
| should_fallback = ( status == 0 or status in (401, 403, 429) or "waf" in body_text or "<!doctype" in body_text or "forbidden" in body_text or "unauthorized" in body_text )
|
关键判断逻辑:
"waf" in body_text:阿里云 WAF 拦截页面通常包含 aliyun_waf 关键字
"<!doctype" in body_text:正常 API 返回 JSON,返回 HTML 说明被拦截
status in (401, 403, 429):这些状态码在正常 API 调用中不应出现
混合引擎路由
API 调用优先走 HTTPX(速度快),WAF 拦截时回退到 Browser:
1 2 3 4 5 6 7 8 9
| async def api_call(self, method, path, token, body=None): result = await self.httpx_engine.api_call(method, path, token, body)
if should_fallback: log.warning("[HybridEngine] api_call 回退到 browser") return await self.browser_engine.api_call(method, path, token, body)
return result
|
流式请求优先走 Browser(成功率高),失败时回退到 HTTPX:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| async def fetch_chat(self, token, chat_id, payload, buffered=False): saw_success = False browser_error = None
try: async for item in self.browser_engine.fetch_chat(token, chat_id, payload): if is_hard_failure and not saw_success: browser_error = item break yield item except Exception: ...
async for item in self.httpx_engine.fetch_chat(token, chat_id, payload): yield item
|
WAF 容错处理
在 Token 验证环节,遇到 WAF 拦截时不直接判定 Token 无效,而是放行交给底层浏览器引擎处理:
1 2 3 4 5 6 7 8 9 10 11 12
| if "aliyun_waf" in resp.text.lower() or "<!doctype" in resp.text.lower(): log.info("[verify_token] 遇到 WAF 拦截页面,放行交给底层无头浏览器引擎处理。") return True
try: data = resp.json() return data.get("role") == "user" except Exception: txt = resp.text.lower() return 'aliyun_waf' in txt or '<!doctype' in txt
|
这个设计很重要:WAF 拦截不等于 Token 失效,如果误判会导致健康的 Token 被踢出账号池。
0x09 curl_cffi HTTPX 引擎
作为 Browser 引擎的补充,HTTPX 引擎用 curl_cffi 模拟 Chrome TLS 指纹,速度快但 WAF 绕过能力有限。
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| _HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://chat.qwen.ai/", "Origin": "https://chat.qwen.ai", "sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", }
_IMPERSONATE = "chrome124"
|
两种引擎对比
| 特性 |
Camoufox (Browser) |
curl_cffi (HTTPX) |
| TLS 指纹 |
Firefox 真实指纹 |
Chrome 模拟指纹 |
| JavaScript |
完整支持 |
不支持 |
| Cookie 管理 |
自动处理 |
手动处理 |
| 资源消耗 |
高(内存 200MB+) |
低(内存 10MB) |
| 启动速度 |
慢(2-5 秒) |
快(毫秒级) |
| WAF 绕过 |
高成功率 |
中等成功率 |
| 适用场景 |
WAF 严格、高频调用 |
WAF 宽松、低频测试 |
0x0A HTTPX 与 Camoufox 协作机制
混合引擎不是简单的”一个不行换另一个”,而是一套有明确优先级和回退逻辑的协作体系。两种引擎各有所长,协作的关键在于”什么场景用谁、什么时候切换、切换后怎么恢复”。
为什么需要两个引擎
单一引擎无法同时满足速度和稳定性:
| 需求 |
Camoufox |
curl_cffi |
| 快速响应(< 100ms) |
不行,页面池获取 + JS 执行需要 200ms+ |
可以,毫秒级建立连接 |
| 绕过 WAF |
高成功率 |
中等成功率,可能被 JS 挑战拦截 |
| 流式 SSE |
需要完整收取后返回,延迟高 |
原生支持逐块流式,首字延迟低 |
| 资源占用 |
200MB+ 内存 |
10MB 内存 |
| 并发能力 |
受页面池大小限制(默认 3) |
几乎无上限 |
所以核心思路是:能用快的就用快的,被拦了再用稳的。
启动顺序
HybridEngine 启动时,先启动 HTTPX 再启动 Browser:
1 2 3 4 5
| async def start(self): await self.httpx_engine.start() await self.browser_engine.start()
|
这个顺序有讲究:HTTPX 启动几乎是瞬时的,Browser 启动需要下载浏览器内核、初始化页面池。先启动 HTTPX 可以让系统尽早进入可用状态,Browser 在后台完成初始化。
两种请求类型的协作策略
api_call:HTTPX 优先,Browser 兜底
api_call 用于非流式的 API 调用(如创建会话、验证 Token),这类请求对速度敏感,WAF 拦截概率相对低。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 客户端请求 api_call │ ▼ ┌─────────────┐ │ HTTPX 引擎 │ ← 首选:速度快(< 100ms) └──────┬──────┘ │ 响应是否正常? ├── 是 → 直接返回 └── 否(WAF 拦截 / 403 / 429) │ ▼ ┌──────────────┐ │ Browser 引擎 │ ← 兜底:在浏览器 JS 中重新执行 fetch └──────┬───────┘ │ ▼ 返回结果
|
完整代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| async def api_call(self, method, path, token, body=None): result = await self.httpx_engine.api_call(method, path, token, body) status = result.get("status") body_text = (result.get("body") or "").lower()
should_fallback = ( status == 0 or status in (401, 403, 429) or "waf" in body_text or "<!doctype" in body_text or "forbidden" in body_text or "unauthorized" in body_text )
if should_fallback: return await self.browser_engine.api_call(method, path, token, body)
return result
|
为什么 api_call 优先 HTTPX:
- 速度:创建会话等操作对延迟敏感,HTTPX 毫秒级响应 vs Browser 200ms+
- 频率:这类请求 WAF 拦截概率低,大多数时候 HTTPX 就够了
- 资源:不占用浏览器页面池,把宝贵的页面留给流式请求
fetch_chat:Browser 优先,HTTPX 兜底
fetch_chat 用于流式对话请求,这是核心业务路径。WAF 对这类高频 POST 请求拦截更严格,Browser 的成功率远高于 HTTPX。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 客户端请求 fetch_chat │ ▼ ┌──────────────┐ │ Browser 引擎 │ ← 首选:WAF 绕过成功率高 └──────┬───────┘ │ 是否收到有效数据? ├── 是(status=200/streamed)→ 持续 yield 数据 └── 否(WAF 拦截 / JS 错误 / 超时) │ ▼ ┌─────────────┐ │ HTTPX 引擎 │ ← 兜底:Chrome TLS 指纹直连 └──────┬──────┘ │ ▼ 返回结果
|
完整代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| async def fetch_chat(self, token, chat_id, payload, buffered=False): saw_success = False browser_error = None
try: async for item in self.browser_engine.fetch_chat(token, chat_id, payload): status = item.get("status")
if status in ("streamed", 200): saw_success = True yield item continue
body_text = (item.get("body") or "").lower() is_hard_failure = ( status in (401, 403, 429) or "waf" in body_text or "<!doctype" in body_text or "forbidden" in body_text or "unauthorized" in body_text )
if is_hard_failure and not saw_success: browser_error = item break
if status == 0 and not saw_success: browser_error = item break
yield item
if browser_error is None: return
except Exception as e: if saw_success: return browser_error = {"status": 0, "body": str(e)}
async for item in self.httpx_engine.fetch_chat(token, chat_id, payload): yield item
|
为什么 fetch_chat 优先 Browser:
- WAF 严格:流式 POST 是 WAF 重点监控对象,HTTPX 很容易被拦
- Cookie 完整:Browser 页面池中已预加载 chat.qwen.ai,Cookie 和会话状态完整
- JS 挑战:WAF 可能下发 JS 挑战,只有 Browser 能自动处理
关键设计:saw_success 防止误回退
saw_success 是一个很重要的状态标志。它的作用是防止”已经收到部分数据后又回退”导致数据重复。
考虑这个场景:
1 2 3 4
| Browser 引擎开始流式返回 → 收到 chunk 1 ✅ (saw_success = True) → 收到 chunk 2 ✅ → 收到 chunk 3 时 WAF 突然拦截
|
如果没有 saw_success,系统会回退到 HTTPX 重新请求,导致:
- chunk 1 和 chunk 2 已经返回给客户端
- HTTPX 重新请求会从头开始,chunk 1 和 chunk 2 重复返回
有了 saw_success,一旦已经成功返回过数据,即使后续出错也不再回退,避免数据重复。
回退触发条件
| 条件 |
api_call 中 |
fetch_chat 中 |
含义 |
status == 0 |
触发回退 |
触发回退 |
网络错误 / JS 执行失败 |
status in (401, 403, 429) |
触发回退 |
触发回退 |
认证/权限/限流 |
"waf" in body |
触发回退 |
触发回退 |
WAF 拦截标识 |
"<!doctype" in body |
触发回退 |
触发回退 |
返回 HTML 拦截页 |
"forbidden" in body |
触发回退 |
触发回退 |
禁止访问 |
"unauthorized" in body |
触发回退 |
触发回退 |
未授权 |
注意 fetch_chat 多了一个隐含条件:只有 saw_success == False 时才回退。已经成功返回过数据就不再切换引擎。
完整请求链路
从客户端请求到最终响应,完整的调用链路如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 客户端请求 → v1_chat.py │ ▼ QwenClient.chat_stream_events_with_retry() │ ├── AccountPool.acquire_wait() ← 获取可用账号 ├── 本地节流检查(最小间隔 1200ms) │ ├── QwenClient.create_chat() ← 创建会话 │ │ │ ▼ │ HybridEngine.api_call() ← HTTPX 优先,Browser 兜底 │ ├── HTTPX: curl_cffi POST /api/v2/chats/new │ └── Browser: page.evaluate(JS_FETCH) │ └── Engine.fetch_chat() ← 发送对话请求 │ ▼ HybridEngine.fetch_chat() ← Browser 优先,HTTPX 兜底 ├── Browser: page.evaluate(JS_STREAM_FULL) │ └── 在 Firefox JS 上下文中 fetch → 完整收取 SSE → 一次性返回 └── HTTPX: curl_cffi stream POST /api/v2/chat/completions └── 逐块流式返回 SSE chunks
|
两种引擎的 SSE 流式差异
Browser 和 HTTPX 处理 SSE 流的方式完全不同,这是协作中最需要注意的差异:
| 维度 |
Browser (Camoufox) |
HTTPX (curl_cffi) |
| 流式方式 |
JS 中完整收取后一次性返回 |
原生逐块流式 |
| 首字延迟 |
高(需等整个流收完) |
低(收到第一块就返回) |
| 内存占用 |
整个响应体在 JS 内存中 |
逐块处理,内存友好 |
| 超时控制 |
JS 内 30 分钟 AbortController |
curl_cffi 1800s timeout |
| 错误处理 |
JS try/catch → 返回 status:0 |
Python except → yield status:0 |
| 数据格式 |
{"status": 200, "body": "完整SSE文本"} |
{"status": "streamed", "chunk": "单块文本"} |
Browser 的 JS_STREAM_FULL 在 JS 中完整收取 SSE 流后一次性返回 body,这是因为 Camoufox 的 expose_function 跨语言回调有限制,无法像 Playwright 那样方便地从 JS 回调到 Python。所以选择了”先收完再传”的方案。
HTTPX 则利用 curl_cffi 的原生流式能力,逐块 yield,首字延迟更低。
协作中的状态感知
HybridEngine 暴露了 status() 方法,让上层能感知两个引擎的实时状态:
1 2 3 4 5 6 7 8 9 10 11 12
| def status(self) -> dict: return { "started": self._started, "mode": "hybrid", "stream_via": "browser_first", "api_via": "httpx_first", "browser_started": ..., "httpx_started": ..., "pool_size": ..., "free_pages": ..., "queue": ..., }
|
free_pages 和 queue 是关键指标:
free_pages == 0:所有页面都在使用中,后续 Browser 请求会排队等待
queue > 0:有请求在等待页面,系统可能过载
当 Browser 引擎过载时,虽然不会自动切换到 HTTPX,但上层可以通过这个状态做降级决策。
0x0B 自动登录与凭证自愈
Camoufox 不仅用于 API 调用,还用于自动登录和账号激活。
自动注册流程
1 2 3
| ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 生成临时邮箱 │───▶│ 填写注册表单 │───▶│ 轮询验证邮件 │───▶│ 完成账号激活 │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| async def register_qwen_account() -> Optional[Account]: """自动注册千问账号""" async with _AsyncMailClient() as mail_client: email = await mail_client.generate_email()
async with _new_browser() as browser: page = await browser.new_page() await page.goto(f"{BASE_URL}/auth?action=signup", wait_until="domcontentloaded")
await name_input.fill(username) await email_input.fill(email) await pwd_input.fill(password) await confirm_input.fill(password) await checkbox.click()
await submit.click() await asyncio.sleep(6)
token = await page.evaluate("localStorage.getItem('token')")
if not token: verify_link = await mail_client.get_verify_link(timeout_sec=300) if verify_link: await page.goto(verify_link) token = await page.evaluate("localStorage.getItem('token')")
|
凭证自愈
当 Token 失效时,自动尝试刷新或重新登录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| async def auto_heal_account(self, acc): """自动修复失效账号""" new_token = await self._try_refresh_token(acc) if new_token: acc.token = new_token acc.invalid = False return
new_token = await self._try_login(acc.email, acc.password) if new_token: acc.token = new_token acc.invalid = False return
log.error(f"[AuthResolver] 账号 {acc.email} 无法自愈")
|
0x0C Docker 部署注意事项
Camoufox 在 Docker 中运行需要额外配置。
系统依赖
Firefox 运行需要大量系统库:
1 2 3 4 5 6
| RUN apt-get update && apt-get install -y \ libx11-xcb1 libx11-6 libxcb1 libxrandr2 \ libxcomposite1 libxdamage1 libxfixes3 \ libgtk-3-0 libasound2 libnss3 libnspr4 \ libdbus-glib-1-2 libxt6 libatk1.0-0 \ && rm -rf /var/lib/apt/lists/*
|
浏览器内核下载
构建时需要下载 Camoufox 浏览器内核:
1
| RUN python -m camoufox fetch
|
共享内存
Firefox 需要足够的共享内存,默认 64MB 不够用:
1 2 3 4
| services: qwen2api: shm_size: 256m
|
如果共享内存不足,Firefox 会崩溃或行为异常。
推荐生产配置
1 2 3 4 5 6
| ENGINE_MODE=hybrid BROWSER_POOL_SIZE=2 MAX_INFLIGHT=1 ACCOUNT_MIN_INTERVAL_MS=1200 REQUEST_JITTER_MIN_MS=120 REQUEST_JITTER_MAX_MS=360
|
0x0D 反检测机制汇总
| 机制 |
代码位置 |
说明 |
| 请求抖动 |
browser_engine.py |
120-360ms 随机延迟 |
| 账号最小间隔 |
account_pool.py |
1200ms 同账号最小请求间隔 |
| 人类化行为 |
browser_engine.py |
Camoufox humanize=True |
| TLS 指纹 |
httpx_engine.py |
curl_cffi impersonate="chrome124" |
| 浏览器指纹 |
browser_engine.py |
Camoufox 完整 Firefox 指纹 |
| WAF 回退 |
hybrid_engine.py |
检测 WAF 拦截自动切换引擎 |
| 页面池复用 |
browser_engine.py |
保持会话状态,避免重复认证 |
| 并发控制 |
account_pool.py |
单账号最大并发 1 |
| 限流冷却 |
account_pool.py |
指数退避冷却(600s 基础,最大 3600s) |
| 凭证自愈 |
auth_resolver.py |
自动刷新 Token + 自动激活 |
| WAF 容错 |
qwen_client.py |
WAF 拦截时不误判 Token 无效 |
0x0E 总结
核心思路
绕过阿里云 WAF 的核心不是”伪造某个特征”,而是”让请求完全从真实浏览器环境中发出”。
传统方案的思路是:用 HTTP 客户端模拟浏览器的各个特征(TLS 指纹、HTTP 头、Cookie)。但 WAF 的检测是多维联合的,只要有一个维度对不上就会被拦截。
Camoufox 方案的思路是:直接在真实浏览器中执行请求。浏览器本身就是最完美的”模拟器”,不存在特征不一致的问题。
技术要点
- JS 注入 Fetch:在浏览器 JS 上下文中执行
fetch,Cookie、TLS、Origin 全部自然正确
- 页面池复用:保持会话状态,避免重复触发 WAF 验证
- 混合引擎:HTTPX 速度快但可能被拦,Browser 稳定但资源消耗大,两者互补
- 请求抖动:打破自动化请求的时间规律
- WAF 容错:WAF 拦截不等于 Token 失效,避免误杀健康账号
一句话概括
Reference