AI编程 · 架构思考 · 技术人生

不装浏览器也能过 Cloudflare:50 行代码的轻量级反爬方案

智谱 GLM,支持多语言、多任务推理。从写作到代码生成,从搜索到知识问答,AI 生产力的中国解法。

Cloudflare挑战绕过方案示意图

前言

Cloudflare 的反爬验证让很多人头疼。遇到 403 或者那个转圈圈的挑战页面,常规做法是掏出 Puppeteer 或 Playwright,启动一个完整的浏览器内核,等它自动完成验证,拿到 Cookie 再继续。

这套方案能用,但有两个问题:

  1. :动辄 200MB+ 的浏览器依赖,部署到云函数或容器里成本不低
  2. :启动浏览器、加载页面、执行 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 构造函数动态执行,注意我们把伪造的 documentlocation 注入进去:

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 说的”好的设计不是加东西,而是减东西”。

局限性

这套方案不是银弹,有几个明确的边界:

  1. 适用范围:只能对付基于 JS 的挑战验证,不适用于 CAPTCHA(那些让你点图片的)
  2. 混淆强度:如果 Cloudflare 升级了混淆策略,引入了环境检测(比如检查 window.navigator),可能需要补充更多伪造逻辑
  3. 合规性:绕过反爬措施在某些场景下可能违反服务条款,使用前确认你有合法的访问权限

实战建议

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 的隔离机制。它们都是类似的”提供一个受控环境,让不可信代码跑起来”的思路。

赞(0)
未经允许不得转载:Toy's Tech Notes » 不装浏览器也能过 Cloudflare:50 行代码的轻量级反爬方案
免费、开放、可编程的智能路由方案,让你的服务随时随地在线。

评论 抢沙发

十年稳如初 — LocVPS,用时间证明实力

10+ 年老牌云主机服务商,全球机房覆盖,性能稳定、价格厚道。

老品牌,更懂稳定的价值你的第一台云服务器,从 LocVPS 开始