跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++Node.js大前端

Web 环境模拟实战:用 C++ Addon 完美还原 document.all

针对 Node.js 环境中模拟浏览器 document.all 对象时遇到的 typeof 检测难题,纯 JS 方案无法实现“存在但 typeof 为 undefined”的特性。通过编写 C++ Addon 调用 V8 引擎的 MarkAsUndetectable 接口,在底层解决了这一反直觉行为。结合 JSDOM 与原生 C++ 逻辑,实现了支持索引访问、函数调用及原型链伪造的完整模拟方案,有效绕过常见的反爬检测机制,同时探讨了原型链处理中的潜在限制与优化方向。

zhang发布于 2026/4/10更新于 2026/7/2543 浏览
Web 环境模拟实战:用 C++ Addon 完美还原 document.all

在 Web 反爬对抗中,环境模拟是绕不开的一环。之前尝试过魔改 Node.js 源码的思路,虽然可行但编译周期太长。这次决定先走轻量路线,通过 Node.js C++ Addon 的方式实现一套基础方案,后续再考虑移植到原生。这种方式的优势在于,只要编译好 .node 扩展,任何 Node 环境都能直接 require 使用。

为什么 document.all 这么难模拟?

document.all 是 IE4 时代的遗留产物。为了兼容旧网页,现代浏览器(Chrome/Firefox)保留了它,但 W3C 和厂商设计了一个非常反直觉的特性:Undetectable。这意味着它在 JS 层面既存在,又表现得像不存在。

在 Chrome 控制台里试一下就能感受到这种诡异:

// 既存在,又不存在
typeof document.all === 'undefined' // true
document.all === undefined          // false (严格相等)
document.all == undefined           // true (宽松相等)

// 看起来是 falsy,但能取值
if(document.all) { /* 不会执行 */ }
document.all.length                 // 正常返回数字
document.all[0]                     // 正常返回元素

这就是纯 JS 模拟的死穴。无论怎么用 Proxy 拦截,或者用 Object.defineProperty,在 JS 层面你永远无法让一个对象的 typeof 变成 'undefined'。JSDOM 至今没有完美支持这一点(它返回的是 'object'),这也是很多反爬脚本检测 JSDOM 的核心依据。

常见的检测逻辑

对方想抓你,通常只需要一行代码:

// 绝杀检测
if(typeof document.all !== 'undefined' && .) {
    .();
}
document
all
console
log
"检测到模拟环境,封禁!"

或者更隐蔽的原型链检测:

// 原型链检测
if(Object.prototype.toString.call(document.all)!=="[object HTMLAllCollection]"){
    return false; // 伪造失败
}

解决方案:从 C++ 层面入手

既然 JS 层面无解,我们就下沉到 V8 引擎层面。V8 提供了 MarkAsUndetectable 接口,专门就是为了实现这种怪异行为的。

核心 C++ 实现

核心逻辑其实就这几步:

// 1. 创建对象模板
Local<ObjectTemplate> template = ObjectTemplate::New(isolate);

// 2. 注入灵魂:标记为 Undetectable
// 这一步之后,typeof 就会返回 undefined,且在布尔判断中为 false
template->MarkAsUndetectable();

// 3. 拦截函数调用:支持 document.all("id") 或 document.all(0)
template->SetCallAsFunctionHandler(CallHandler);

// 4. 拦截索引访问:支持 document.all[0]
template->SetHandler(IndexedPropertyHandlerConfiguration(IndexedGetter));

// 5. 实例化并导出
Local<Object> instance = template->NewInstance(context).ToLocalChecked();

通过这个 Addon 生成的对象,在 Node.js 环境里就能表现得和浏览器一模一样。

对接 JS 层

C++ 只负责提供'虽然存在但 typeof 是 undefined'的容器,具体的 DOM 查询逻辑(比如根据 ID 找元素)还是写在 JS 里比较方便。我们可以这样把两者结合起来:

const { JSDOM } = require('jsdom');
const addon = require('./addon');

const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p></html>`);
window = dom.window;

// 保存原始 document 引用
const realDocument = dom.window.document;

function myAllHandler(arg) {
    console.log('myAllHandler arg', arg);
    if(arg === undefined) {
        return realDocument.getElementsByTagName('*');
    }
    if(typeof arg === 'number') {
        const all = realDocument.getElementsByTagName('*');
        return all[arg] || null;
    }
    if(typeof arg === 'string') {
        const byId = realDocument.getElementById(arg);
        if(byId) return byId;
        const byName = realDocument.getElementsByName(arg);
        if(byName && byName.length > 0) return byName[0];
        return null;
    }
    return null;
}

function internalAllHandler(opOrIndex, maybeArg) {
    if(opOrIndex === 'INVOKE') {
        return myAllHandler(maybeArg);
    }
    return myAllHandler(opOrIndex);
}

// 绑定回调与日志
addon.SetAllHandler(internalAllHandler);
addon.SetTraceLog(true);
addon.SetVerboseLog(false);

// 替换 document.all
document = addon.watch(realDocument);
Object.defineProperty(document, 'all', {
    get: function() {
        return addon.khall;
    },
    configurable: true
});

console.log(Object.prototype.toString.call(document.all));
console.log('\n--- 1. Access & Call ---');
console.log('document.all[0]:', document.all[0]);
console.log('document.all["app"]:', document.all["app"]);
console.log('document.all(0):', document.all(0));
console.log('document.all("app"):', document.all("app"));
console.log('document.all.length:', document.all.length);

console.log('\n--- 2. Typeof & Undefined Check ---');
console.log('typeof document.all:', typeof document.all); // 预期:undefined
console.log('document.all === undefined:', document.all === undefined); // false
console.log('document.all == undefined:', document.all == undefined); // true
console.log('document.all == null:', document.all == null); // true
console.log('document.all === null:', document.all === null); // false

console.log('\n--- 3. Boolean Logic ---');
console.log('Boolean(document.all):', Boolean(document.all)); // false
console.log('!!document.all:', !!document.all); // false
console.log('!document.all:', !document.all); // true

if(document.all) {
    console.log('Check: if (document.all) is TRUE [❌ Fail or Old IE]');
} else {
    console.log('Check: if (document.all) is FALSE [✅ Pass - Modern Behavior]');
}

console.log('document.all || "fallback":', document.all || "fallback");
console.log('document.all && "hidden":', document.all && "hidden");

console.log('\n--- 4. Prototype & Object Tag ---');
console.log('Object.prototype.toString.call(document.all):', Object.prototype.toString.call(document.all));
console.log('Symbol.toStringTag:', document.all[Symbol.toStringTag]);
console.log('document.all.constructor.name:', document.all.constructor ? document.all.constructor.name : 'undefined');

console.log('\n--- 5. Enumeration ---');
try {
    console.log('Object.keys(document.all).length:', Object.keys(document.all).length);
} catch(e) {
    console.log('Object.keys failed:', e.message);
}

验证结果

跑一下测试,看看效果是否符合预期:

[object HTMLAllCollection]
--- 1. Access & Call ---
[KhBox Trace] IndexedGetter called with index:0
myAllHandler arg 0 ✅ Pass
||| document.all[0]: HTMLHtmlElement {}
document.all["app"]:undefined
[KhBox Trace] CALL:khall(0)
myAllHandler arg 0 ✅ Pass
||| document.all(0): HTMLHtmlElement {}
[KhBox Trace] CALL:khall(app)
myAllHandler arg app ✅ Pass
||| document.all("app"): HTMLParagraphElement {}
document.all.length:0

--- 2. Typeof & Undefined Check ---
✅ Pass ||| typeof document.all:undefined
✅ Pass ||| document.all === undefined:false
✅ Pass ||| document.all == undefined:true
✅ Pass ||| document.all == null:true
✅ Pass ||| document.all === null:false

--- 3. Boolean Logic ---
Boolean(document.all):false
!!document.all:false
!document.all:true
Check: if (document.all) is FALSE [✅ Pass - Modern Behavior]
document.all || "fallback": fallback
document.all && "hidden": HTMLAllCollection {length:0...}

--- 4. Prototype & Object Tag ---
Object.prototype.toString.call(document.all):[object HTMLAllCollection]
Symbol.toStringTag: HTMLAllCollection
document.all.constructor.name: HTMLAllCollection

--- 5. Enumeration ---
Object.keys(document.all).length:2

新的问题与思考

这个 length 目前是 0,很好改,改成 document.getElementsByTagName('*').length 的值就可以了。不过要注意,这个 length 属性应该是在原型链上,而不是实例对象上。在 HTMLAllCollection 里面定义才符合规范。

所以,这个 addon 思路和魔改 Node 的最大缺陷就是:不知道某个方法是在实例,还是在它的哪一层原型上。 这必须要在 JS 层来处理。

不过,如果是和之前的思路结合,应该能省下非常多的代码。比如:

const handler = function(obj, prop, args) {
    const className = obj.constructor.name;
    const key = `${className}_${prop}`;
    
    // 1. 日志
    console.log(`{get: ${key}}`);
    
    // 2. 如果有自定义实现
    if(khBox.envFuncs[key]) {
        return khBox.envFuncs[key].apply(obj, args);
    }
    
    // 3. 否则走 JSDOM
    return Reflect.get(khBox.memory.jsdom[className], prop);
};

// 转到 C 层去拦截
khboxAddon.setupInterceptor(document, handler);

这样就简单地实现了自己的函数 + JSDOM 结合补全,且不用 Proxy 来层层追踪代理。因为 C 层自动加了 get、set 等的回调,自动输出调用链。相当于是实现了之前 JS 框架里的 proxy(获取调用关系)、dispatch(先保护后分发)、setnative(保护)等的工具函数。

唯一的问题就是找到原型上的所有方法并保存下来,把它写成符合这个思路的模板。后续会不断更改这个框架,争取早日完善成型。

目录

  1. 为什么 document.all 这么难模拟?
  2. 常见的检测逻辑
  3. 解决方案:从 C++ 层面入手
  4. 核心 C++ 实现
  5. 对接 JS 层
  6. 验证结果
  7. 新的问题与思考
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • FPGA 自学:Vivado RAM IP 核配置与仿真测试
  • OpenClaw 中 web_search + web_fetch 最佳实践速查表
  • OpenCore 在 PC 上安装 macOS 完整指南
  • Windows 本地部署 Ollama 与 OpenClaw,构建 AI 生产力系统
  • 详解 Python 常见文件后缀:.py、.ipynb、.pyi、.pyc、.pyd
  • 强干扰环境下光纤无人机通信模块技术解析
  • 沁恒微 RISC-V 蓝牙应用常用参数设定与修改
  • 深入理解 Java 语言的核心概念
  • Silly Tavern 角色卡与世界书导入教程
  • FPGA 核心概念解析:FMC、VITA、UltraScale、MPSoC 及接口标准
  • 飞书机器人图片消息发送实战与常见报错解决
  • Ubuntu20.04 Gazebo 仿真宇树机器狗及外置激光雷达部署
  • OpenAI 集成 Langchain 操作实战详解
  • Webots 2025a + ROS 2 Jazzy e-puck 机器人集成与导航研究教程
  • 数据结构:八种常见排序算法
  • RK3562 单板机 Linux 应用开发环境搭建与 GDB 调试指南
  • Vitis AI 在 ZCU102 上的推理部署实录
  • 基于 Vivado 的 FPGA 比特流在线更新升级
  • AI 大模型开源知识库 RAGFlow 从部署到实战详解
  • 魔因漫创集成中转平台实现低成本AI漫画视频创作
  • 相关免费在线工具

    • Base64 字符串编码/解码

      将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

    • Base64 文件转换器

      将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

    • Markdown转HTML

      将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

    • HTML转Markdown

      将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

    • JSON 压缩

      通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

    • JSON美化和格式化

      将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online