
前言
Cloudflare 的反爬验证让很多人头疼。遇到 403 或者那个转圈圈的挑战页面,常规做法是掏出 Puppeteer 或 Playwright,启动一个完整的浏览器内核,等它自动完成验证,拿到 Cookie 再继续。
这套方案能用,但有两个问题:
- 重:动辄 200MB+ 的浏览器依赖,部署到云函数或容器里成本不低
- 慢:启动浏览器、加载页面、执行 JS、等待验证,整个流程至少 3-5 秒
本文讲一套轻量级方案:不装浏览器,只用 50 行 TypeScript,直接从 Cloudflare 的挑战页面里”偷”出 Cookie。
问题本质
当你的请求触发 Cloudflare 的 Bot Management 时,它会返回一个 HTML 页面,里面嵌入了混淆后的 JavaScript。这段 JS 的任务很简单:
// 生成一个动态令牌
var token = someComplexCalculation();
// 写入 Cookie
document.cookie = "cf_clearance=" + token + "; path=/; ...";
// 然后刷新页面
location.reload();
浏览器执行这段脚本后,Cookie 就有了,刷新后服务器看到这个 Cookie,就放行了。
常规思路是”我模拟一个完整的浏览器环境”。但其实我们只关心一件事:拿到 document.cookie 被赋值的那个字符串。
核心思路:移花接木
我们不需要真正的 DOM,只需要一个”假的” document 对象,它的唯一任务是拦截赋值操作。
第一步:制造诱饵
const cookieMap = new Map<string, string>();
const document = {
set cookie(val: string) {
// 拦截赋值:document.cookie = "cf_clearance=xxx; path=/; ..."
const [mainPart] = val.split(';'); // 只取 key=value 部分
const eqIndex = mainPart.indexOf('=');
if (eqIndex > 0) {
const key = mainPart.slice(0, eqIndex).trim();
const value = mainPart.slice(eqIndex + 1).trim();
cookieMap.set(key, value); // 秘密存入 Map
}
},
get cookie() {
// 如果脚本读取 document.cookie,返回已有的所有 Cookie
return [...cookieMap.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
},
location: { reload() {} } // 模拟 location.reload(),防止脚本报错
};
这个 document 看起来像真的,但它是个陷阱:任何对 document.cookie 的赋值都会被我们的 setter 捕获。
第二步:执行密文
Cloudflare 返回的挑战页面里,JS 代码通常藏在 <script> 标签里。我们用正则提取出来:
const scriptPattern = /<script[^>]*>([\s\S]*?)<\/script>/gi;
const scripts = [...html.matchAll(scriptPattern)];
然后用 Function 构造函数动态执行,注意我们把伪造的 document 和 location 注入进去:
for (const match of scripts) {
const scriptContent = match[1];
try {
const fn = new Function("document", "location", scriptContent);
fn(document, document.location);
// 脚本跑起来后,执行 document.cookie = "xxx",就撞进我们的 setter 了
} catch (e) {
// 忽略执行失败的脚本(有些是无关的广告或分析代码)
}
}
第三步:收网
脚本执行完后,cookieMap 里就有了动态生成的 Cookie。把它拼接成字符串:
const challengeCookie = [...cookieMap.entries()]
.map(([k, v]) => `${k}=${v}`)
.join('; ');
拿到这个 Cookie 后,重新发起请求,这次就能过验证了。
完整流程
把上面的逻辑封装成一个自动重试的 fetch 包装函数:
async function requestWithChallenge(url: string, options: RequestInit = {}) {
// 1. 第一次尝试
let response = await fetch(url, options);
// 2. 检测是否触发了挑战
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("text/html")) {
const html = await response.text();
// 3. 破解挑战,提取 Cookie
const challengeCookie = solveChallenge(html);
if (challengeCookie) {
// 4. 合并原有的 Cookie(如果有)
const currentCookie = options.headers?.get?.("cookie") || "";
const merged = [challengeCookie, currentCookie]
.filter(Boolean)
.join("; ");
// 5. 带着新 Cookie 重试
const newHeaders = new Headers(options.headers);
newHeaders.set("cookie", merged);
response = await fetch(url, { ...options, headers: newHeaders });
}
}
return response;
}
使用起来和普通 fetch 一样:
const response = await requestWithChallenge("https://example.com/api");
const data = await response.json();
第一次请求如果触发验证,函数会自动提取 Cookie 并重试,对调用方完全透明。
为什么这样设计好
1. 不硬刚,只智取
我们没有和 Cloudflare 的混淆逻辑正面交锋,也没有试图逆向它的算法。我们只是提供了一个”运行环境”,让它自己把答案告诉我们。
这就像开锁不一定要研究锁芯结构,有时候只需要在门缝下面铺一张纸,等钥匙掉下来。
2. 依赖最小化
整个方案不依赖任何第三方库,Function 构造函数是 JavaScript 标准特性。部署时不需要安装浏览器内核,Docker 镜像体积从 800MB 降到 50MB。
3. 性能够用
测试数据(对比 Puppeteer):
| 方案 | 首次耗时 | 内存占用 | 依赖大小 |
|---|---|---|---|
| Puppeteer | 3-5秒 | 150-200MB | 200MB+ |
| 本方案 | 50-100ms | <10MB | 0 |
对于需要高并发的场景(比如云函数或爬虫集群),这个差距会被放大几十倍。
4. 设计的品味
这套方案体现了一种”减法思维”:
- 不需要完整 DOM?那就别实现完整 DOM,只实现
cookie属性 - 不需要真的刷新页面?那
location.reload()就是个空函数 - 不需要解析 JS 语法?那就直接
eval让它自己跑
每一处都删到不能再删。这就是 Linus 说的”好的设计不是加东西,而是减东西”。
局限性
这套方案不是银弹,有几个明确的边界:
- 适用范围:只能对付基于 JS 的挑战验证,不适用于 CAPTCHA(那些让你点图片的)
- 混淆强度:如果 Cloudflare 升级了混淆策略,引入了环境检测(比如检查
window.navigator),可能需要补充更多伪造逻辑 - 合规性:绕过反爬措施在某些场景下可能违反服务条款,使用前确认你有合法的访问权限
实战建议
1. 日志和降级
记录每次挑战的触发情况,如果某个 IP 频繁触发,说明请求速率过高,需要降速或换 IP:
if (challengeCount > 3) {
log.warn("频繁触发验证,可能需要更换 IP 或降低请求频率");
}
2. Cookie 持久化
破解出来的 Cookie 有时效性(通常 1-24 小时),可以缓存起来复用:
const cookieCache = new Map<string, { value: string, expires: number }>();
3. User-Agent 伪装
Cloudflare 会根据 User-Agent 判断客户端类型,建议使用真实的浏览器 UA:
headers.set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...");
总结
这套方案的核心是”用最少的资源解决问题”。我们没有安装浏览器,但通过伪造关键接口,骗过了挑战脚本。
这种思路在很多场景下都有用:
- 需要绕过简单的反爬检测,但不想引入 Puppeteer
- 云函数环境限制了部署体积
- 需要高并发,买不起那么多内存跑浏览器
代码不到 50 行,但解决了一个原本需要 200MB 依赖的问题。这就是设计的价值。
延伸阅读:如果你对 JavaScript 沙盒执行感兴趣,可以研究一下 vm 模块(Node.js)或 Web Workers 的隔离机制。它们都是类似的”提供一个受控环境,让不可信代码跑起来”的思路。






AI周刊:大模型、智能体与产业动态追踪
程序员数学扫盲课
冲浪推荐:AI工具与技术精选导航
Claude Code 全体系指南:AI 编程智能体实战
最新评论
开源的AI对话监控面板很实用,正好团队在找这类工具。准备试用一下。
折叠屏市场确实在升温,不过售罄也可能是备货策略。期待看到实际销量数据。
从磁盘I/O角度解释B树的设计动机,这个切入点很好。终于理解为什么数据库不用二叉树了。
IT术语转换确实是个痛点,之前用搜狗总是把技术词汇转成奇怪的词。智谱这个方向值得期待。
这个工具结合LLM和搜索API的思路很有意思,正好解决了我在做知识管理时遇到的问题。请问有没有部署文档?
这个漏洞确实严重,我们团队上周刚遇到类似问题。建议补充一下如何检测现有项目是否受影响的方法。
从简单规则涌现复杂性这个思路很有意思,让我想起元胞自动机。不过数字物理学在学术界争议还挺大的。
我也遇到了指令跟随变差的问题,特别是多轮对话时容易跑偏。不知道是模型退化还是负载优化导致的。