Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

Camoufox 绕过阿里云 WAF 实战

0x00 背景

通义千问(chat.qwen.ai)部署了阿里云 WAF(Web Application Firewall),对 API 请求进行多维检测。直接用 HTTP 客户端请求会被拦截,返回 403 或 WAF 拦截页面。

1
2
3
4
5
6
# 普通 HTTP 请求会被 WAF 拦截
import httpx

async with httpx.AsyncClient() as client:
resp = await client.get("https://chat.qwen.ai/api/v1/auths/")
# 返回 403 Forbidden 或包含 aliyun_waf 的拦截页面

本文记录如何使用 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_cffitls-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 指纹问题,但解决不了:

  1. JS 挑战验证:WAF 可能下发 JS 挑战,纯 HTTP 客户端无法执行
  2. Cookie 生成链:某些 Cookie 是 WAF 通过 JS 动态生成并签名的,手动构造无法通过验证
  3. 行为模式:即使 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": {
# 软件 WebRender(服务器无 GPU)
"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")

关键设计

  1. 页面池复用:避免每次请求创建新页面,减少资源消耗和指纹暴露
  2. 预加载站点:提前访问 chat.qwen.ai,建立 Cookie 和会话状态
  3. 视口设置:1920x1080 模拟真实桌面浏览器分辨率
  4. 异步队列:用 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);" # 30分钟超时
"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

这是整个方案的关键决策,原因有四:

  1. Cookie 自动携带:浏览器自动管理 Cookie,无需手动处理 WAF 通过 JS 设置的验证 Cookie
  2. TLS 指纹一致:使用 Firefox 真实 TLS 实现,指纹完全合法,与页面访问时的 TLS 指纹一致
  3. JavaScript 环境完整:支持 WAF 的 JS 挑战验证,浏览器上下文中有完整的 DOM 和 JS 运行时
  4. 请求来源合法:从 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) # 默认 120ms
high = max(low, settings.REQUEST_JITTER_MAX_MS) # 默认 360ms
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  # 默认 1200ms
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 # 600s
dynamic = min(
settings.RATE_LIMIT_MAX_COOLDOWN, # 最大 3600s
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:

1
2
# account_pool.py
MAX_INFLIGHT = 1 # 单账号最大并发

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 # WAF 拦截标识
or "<!doctype" in body_text # HTML 响应(WAF 拦截页)
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):
# 首先尝试 HTTPX
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):
# 检测 WAF 拦截
if is_hard_failure and not saw_success:
browser_error = item
break
yield item
except Exception:
...

# 回退到 HTTPX
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
# qwen_client.py
if "aliyun_waf" in resp.text.lower() or "<!doctype" in resp.text.lower():
log.info("[verify_token] 遇到 WAF 拦截页面,放行交给底层无头浏览器引擎处理。")
return True

# auth_resolver.py
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 有效

这个设计很重要: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" # curl_cffi 使用 Chrome 124 TLS 指纹

两种引擎对比

特性 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):
# 第一步:启动 HTTPX(毫秒级,立即可用)
await self.httpx_engine.start()
# 第二步:启动 Browser(需要下载/启动 Firefox,2-5 秒)
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):
# 1. 先走 HTTPX
result = await self.httpx_engine.api_call(method, path, token, body)
status = result.get("status")
body_text = (result.get("body") or "").lower()

# 2. 判断是否需要回退
should_fallback = (
status == 0 # 网络错误(DNS/连接失败)
or status in (401, 403, 429) # 认证/权限/限流
or "waf" in body_text # WAF 拦截标识
or "<!doctype" in body_text # 返回 HTML 而非 JSON
or "forbidden" in body_text # 禁止访问
or "unauthorized" in body_text # 未授权
)

# 3. 回退到 Browser
if should_fallback:
return await self.browser_engine.api_call(method, path, token, body)

return result

为什么 api_call 优先 HTTPX:

  1. 速度:创建会话等操作对延迟敏感,HTTPX 毫秒级响应 vs Browser 200ms+
  2. 频率:这类请求 WAF 拦截概率低,大多数时候 HTTPX 就够了
  3. 资源:不占用浏览器页面池,把宝贵的页面留给流式请求

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

# 浏览器引擎自身错误(evaluate 失败等)→ 回退
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)}

# 回退到 HTTPX
async for item in self.httpx_engine.fetch_chat(token, chat_id, payload):
yield item

为什么 fetch_chat 优先 Browser:

  1. WAF 严格:流式 POST 是 WAF 重点监控对象,HTTPX 很容易被拦
  2. Cookie 完整:Browser 页面池中已预加载 chat.qwen.ai,Cookie 和会话状态完整
  3. 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 重新请求,导致:

  1. chunk 1 和 chunk 2 已经返回给客户端
  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", # 流式请求走 browser 优先
"api_via": "httpx_first", # API 调用走 httpx 优先
"browser_started": ..., # 浏览器引擎是否启动
"httpx_started": ..., # HTTPX 引擎是否启动
"pool_size": ..., # 页面池大小
"free_pages": ..., # 当前空闲页面数
"queue": ..., # 等待页面的请求数
}

free_pagesqueue 是关键指标:

  • 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:
# 1. 生成临时邮箱
email = await mail_client.generate_email()

# 2. 使用 Camoufox 打开注册页面
async with _new_browser() as browser:
page = await browser.new_page()
await page.goto(f"{BASE_URL}/auth?action=signup", wait_until="domcontentloaded")

# 3. 填写注册表单
await name_input.fill(username)
await email_input.fill(email)
await pwd_input.fill(password)
await confirm_input.fill(password)
await checkbox.click()

# 4. 提交注册
await submit.click()
await asyncio.sleep(6)

# 5. 获取 Token
token = await page.evaluate("localStorage.getItem('token')")

# 6. 如果需要邮件验证,轮询验证链接
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):
"""自动修复失效账号"""
# 1. 尝试刷新 Token
new_token = await self._try_refresh_token(acc)
if new_token:
acc.token = new_token
acc.invalid = False
return

# 2. 刷新失败,尝试重新登录
new_token = await self._try_login(acc.email, acc.password)
if new_token:
acc.token = new_token
acc.invalid = False
return

# 3. 登录也失败,标记为彻底失效
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
# docker-compose.yml
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 方案的思路是:直接在真实浏览器中执行请求。浏览器本身就是最完美的”模拟器”,不存在特征不一致的问题。

技术要点

  1. JS 注入 Fetch:在浏览器 JS 上下文中执行 fetch,Cookie、TLS、Origin 全部自然正确
  2. 页面池复用:保持会话状态,避免重复触发 WAF 验证
  3. 混合引擎:HTTPX 速度快但可能被拦,Browser 稳定但资源消耗大,两者互补
  4. 请求抖动:打破自动化请求的时间规律
  5. WAF 容错:WAF 拦截不等于 Token 失效,避免误杀健康账号

一句话概括

1
不是伪造浏览器特征,而是让浏览器自己发请求。

Reference