> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# jsdom 完全指南 — JS逆向实战
- URL: https://blog.vercanti.com/jsdom-wan-quan-zhi-nan-jsni-xiang-shi-zhan/
- Published: 2026-08-28T14:35:09.000Z
- Updated: 2026-08-28T14:58:10.000Z
- Description: 1. 什么是 jsdom(#1-%E4%BB%80%E4%B9%88%E6%98%AF-jsdom) 2. 安装与环境(#2-%E5%AE%89%E8%A3%85%E4%B8%8E%E7%8E%AF%E5%A2%83) 3. 基础用法(#3-%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95) 4. 构造函数完整选项参考(#4-%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%AE%8C%E6%95%B4%E9%80%89%E9%A1%B9%E5%8F%82%E8%80%83) 5. 高级用法(#5-%
- Author: yellowdog
- Tags: js逆向, 技巧步骤

> 官方文档：<https://github.com/jsdom/jsdom>  
> 适用版本：jsdom 20.x+（2026-05-08 核实）  
> **最后更新**: 2026-03-05（经官方文档复核）  
> **参考来源**: [jsdom GitHub 官方仓库](https://github.com/jsdom/jsdom) | [npm 包页面](https://www.npmjs.com/package/jsdom) | [keepnight 补环境实战](https://www.keepnight.com/archives/123/) | [知乎·jsdom 补环境详解](https://zhuanlan.zhihu.com/p/590564963)

---

## 目录

1. [什么是 jsdom](#1-%E4%BB%80%E4%B9%88%E6%98%AF-jsdom)
2. [安装与环境](#2-%E5%AE%89%E8%A3%85%E4%B8%8E%E7%8E%AF%E5%A2%83)
3. [基础用法](#3-%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95)
4. [构造函数完整选项参考](#4-%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%AE%8C%E6%95%B4%E9%80%89%E9%A1%B9%E5%8F%82%E8%80%83)
5. [高级用法](#5-%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95)
6. [JS逆向核心场景](#6-js%E9%80%86%E5%90%91%E6%A0%B8%E5%BF%83%E5%9C%BA%E6%99%AF)
7. [Proxy 补环境技术](#7-proxy-%E8%A1%A5%E7%8E%AF%E5%A2%83%E6%8A%80%E6%9C%AF)
8. [反检测与指纹伪造](#8-%E5%8F%8D%E6%A3%80%E6%B5%8B%E4%B8%8E%E6%8C%87%E7%BA%B9%E4%BC%AA%E9%80%A0)
9. [常见坑与解决方案](#9-%E5%B8%B8%E8%A7%81%E5%9D%91%E4%B8%8E%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88)
10. [推荐方法与工具链](#10-%E6%8E%A8%E8%8D%90%E6%96%B9%E6%B3%95%E4%B8%8E%E5%B7%A5%E5%85%B7%E9%93%BE)

---

## 1\. 什么是 jsdom

jsdom 是一个纯 JavaScript 实现的 Web 标准库，专门为 Node.js 设计，用于模拟浏览器的 DOM（文档对象模型）和 HTML 解析环境。它实现了以下标准：

- **WHATWG DOM 标准** — document、Element、Event 等
- **WHATWG HTML 标准** — HTML 解析、脚本执行
- **CSSOM** — 样式计算（有限支持）
- **XMLHttpRequest / fetch** — 网络请求（有限支持）
- **localStorage / sessionStorage** — 存储 API
- **Canvas API** — 接口存在，但不渲染（需额外 canvas 包）

**jsdom 不是无头浏览器。** 它不做布局计算也不渲染像素。但它可以执行 JavaScript 脚本，这正是 JS 逆向的核心利用点。

> 引用：*"The goal of the project is to emulate enough of a subset of a web browser to be useful for testing and scraping real-world web applications."* — [jsdom README](https://github.com/jsdom/jsdom)

---

## 2\. 安装与环境

```bash
# 使用 npm
npm install jsdom

# 使用 pnpm
pnpm add jsdom

# 使用 yarn
yarn add jsdom

# TypeScript 类型定义（可选）
npm install --save-dev @types/jsdom

```

**Node.js 版本要求**: 随 jsdom 版本变化，请以 jsdom `package.json` 中的 `engines` 字段为准。通常建议使用当前 LTS 版本（Node.js 20+）。

---

## 3\. 基础用法

### 3.1 最简单的 DOM 解析

```javascript
const { JSDOM } = require('jsdom');

// 传入 HTML 字符串，返回 DOM 实例
const dom = new JSDOM(`<!DOCTYPE html>
  <body>
    <h1 id="title">Hello jsdom</h1>
    <ul>
      <li class="item">Apple</li>
      <li class="item">Banana</li>
    </ul>
  </body>`);

const { document } = dom.window;

// 像浏览器一样操作 DOM
console.log(document.getElementById('title').textContent); // "Hello jsdom"
console.log(document.querySelectorAll('.item').length);    // 2

```

### 3.2 从 URL 加载页面

```javascript
const { JSDOM } = require('jsdom');

// 直接从远程 URL 加载（适合静态页面）
const dom = await JSDOM.fromURL('https://example.com', {
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0',
  resources: 'usable',
  runScripts: 'dangerously',
});

console.log(dom.window.document.title);

```

### 3.3 从文件加载

```javascript
const { JSDOM } = require('jsdom');
const path = require('path');

const dom = await JSDOM.fromFile(path.resolve('./page.html'), {
  url: 'http://localhost/',
});

```

### 3.4 访问 window 对象

```javascript
const { JSDOM } = require('jsdom');

const dom = new JSDOM(`<html></html>`, {
  url: 'https://example.com/',
});

const { window } = dom;

// 访问标准 API
console.log(window.location.href);   // "https://example.com/"
console.log(window.navigator.userAgent);
console.log(window.document.readyState);

```

---

## 4\. 构造函数完整选项参考

```javascript
const dom = new JSDOM(html, {
  // --- 基础配置 ---
  url: 'https://example.com/',          // 文档 URL（影响 location、cookie 域等）
  referrer: 'https://google.com/',      // document.referrer 的值
  contentType: 'text/html',             // 解析模式：'text/html' 或 'application/xhtml+xml'

  // --- 脚本执行 ---
  runScripts: 'dangerously',            // 执行页面内嵌 <script>（危险！仅对可信内容使用）
  // runScripts: 'outside-only',        // 仅允许外部通过 window.eval() 执行脚本

  // --- 资源加载 ---
  resources: 'usable',                  // 加载可用资源（img、iframe、外部脚本等）

  // --- Cookie 管理 ---
  cookieJar: new CookieJar(),           // 自定义 Cookie Jar，需先 const { JSDOM, CookieJar } = require('jsdom')

  // --- 视觉模拟 ---
  pretendToBeVisual: true,              // 使 document.hidden=false、document.visibilityState="visible"，启用 requestAnimationFrame

  // --- 存储限制 ---
  storageQuota: 5000000,                // localStorage/sessionStorage 配额（code units，默认 5000000）

  // --- 高级资源配置（对象形式，替代 'usable' 字符串） ---
  // resources: {
  //   userAgent: 'Custom UA',
  //   dispatcher: new ProxyAgent('http://127.0.0.1:7890'),  // 代理
  //   interceptors: [requestInterceptor((req) => { ... })], // 请求拦截
  // },

  // --- 调试 ---
  includeNodeLocations: true,           // 在 DOM 节点上保留源码位置信息（影响性能）

  // --- 虚拟控制台 ---
  virtualConsole: customVirtualConsole, // 捕获页面内的 console 输出
});

```

> 引用：*"If you want to execute script 'from the outside,' you can use the `runScripts: 'outside-only'` option, which enables fresh copies of all JavaScript spec-provided globals to be installed on window."* — [jsdom GitHub](https://github.com/jsdom/jsdom)

---

## 5\. 高级用法

### 5.1 VirtualConsole — 捕获脚本输出

```javascript
const { JSDOM, VirtualConsole } = require('jsdom');

const virtualConsole = new VirtualConsole();

// 捕获页面脚本的 console 输出
virtualConsole.on('log', (...args) => {
  console.log('[页面log]', ...args);
});
virtualConsole.on('error', (...args) => {
  console.error('[页面error]', ...args);
});
virtualConsole.on('warn', (...args) => {
  console.warn('[页面warn]', ...args);
});

// 将 jsdom 自身的日志转发到 Node.js console
virtualConsole.sendTo(console, { omitJSDOMErrors: false });

const dom = new JSDOM(`<script>console.log('hello from page')</script>`, {
  runScripts: 'dangerously',
  virtualConsole,
});

```

### 5.2 CookieJar — Cookie 管理

```javascript
const { JSDOM, CookieJar } = require('jsdom');
const tough = require('tough-cookie'); // jsdom 内部依赖

const cookieJar = new CookieJar();

// 预设 Cookie（模拟已登录状态）
cookieJar.setCookieSync(
  'sessionid=abc123; Path=/; Domain=example.com',
  'https://example.com/'
);

const dom = new JSDOM(`<html></html>`, {
  url: 'https://example.com/',
  cookieJar,
  runScripts: 'dangerously',
});

// 从页面脚本中读取 Cookie
console.log(dom.window.document.cookie); // "sessionid=abc123"

// 执行脚本后，读取页面设置的新 Cookie
dom.window.document.cookie = 'token=xyz; Path=/';
const cookies = cookieJar.getCookiesSync('https://example.com/');
console.log(cookies);

```

### 5.3 getInternalVMContext — 直接注入变量

```javascript
const { JSDOM } = require('jsdom');
const vm = require('vm');

const dom = new JSDOM(`<html></html>`, {
  url: 'https://example.com/',
  runScripts: 'outside-only', // 必须开启才能使用 VMContext
});

// 获取内部 VM 上下文（即 window 沙箱）
const vmContext = dom.getInternalVMContext();

// 在 window 沙箱中执行脚本
const script = new vm.Script(`
  var myResult = Math.random() * 1000;
`);
script.runInContext(vmContext);

console.log(dom.window.myResult); // 可访问脚本中定义的变量

```

### 5.4 资源拦截器 — 拦截/修改加载的脚本

> 注意：jsdom 已移除旧版 `ResourceLoader` 类继承模式，改为基于 undici 拦截器的新 API。

```javascript
const { JSDOM, requestInterceptor } = require('jsdom');

const dom = new JSDOM(`<html></html>`, {
  url: 'https://example.com/',
  resources: {
    // 使用拦截器数组拦截特定资源
    interceptors: [
      requestInterceptor((request, context) => {
        if (request.url.includes('anti-crawler.js')) {
          console.log(`[拦截] ${request.url}`);
          // 返回替换内容，阻止原脚本加载
          return new Response('// blocked', {
            headers: { 'Content-Type': 'application/javascript' },
          });
        }
        // 返回 undefined 表示不拦截，正常加载
      }),
    ],
  },
  runScripts: 'dangerously',
});

```

如需配置代理，在 `resources` 对象中使用 `dispatcher` 字段（undici Dispatcher 实例）：

```javascript
const { ProxyAgent } = require('undici');

const dom = new JSDOM(`<html></html>`, {
  url: 'https://example.com/',
  resources: {
    userAgent: 'Mozilla/5.0 ...',
    dispatcher: new ProxyAgent('http://127.0.0.1:7890'),
  },
  runScripts: 'dangerously',
});

```

### 5.5 网络代理配置

```javascript
const { JSDOM } = require('jsdom');
const { ProxyAgent } = require('undici');

// fromURL 和构造函数均支持 resources.dispatcher
const dom = await JSDOM.fromURL('https://example.com/', {
  resources: {
    dispatcher: new ProxyAgent('http://127.0.0.1:7890'), // 通过代理加载所有资源
  },
});

```

---

## 6\. JS逆向核心场景

### 6.1 场景一：模拟浏览器环境执行加密 JS

许多网站的加密逻辑依赖浏览器全局对象（`window`、`document`、`navigator` 等），在 Node.js 中直接运行会报错。jsdom 可以提供这些对象。

```javascript
const { JSDOM } = require('jsdom');
const fs = require('fs');

// 读取从网站扣出的加密脚本
const encryptScript = fs.readFileSync('./encrypt.js', 'utf-8');

const dom = new JSDOM(`<!DOCTYPE html><html></html>`, {
  url: 'https://target-site.com/', // 必须与目标站同域，否则 Cookie/同源策略会出问题
  runScripts: 'dangerously',
  pretendToBeVisual: true,
  virtualConsole: (() => {
    const { VirtualConsole } = require('jsdom');
    const vc = new VirtualConsole();
    vc.sendTo(console);
    return vc;
  })(),
});

const { window } = dom;

// 将脚本注入 window 环境执行
window.eval(encryptScript);

// 调用加密函数获取签名
const signature = window.getSign({ uid: 12345, ts: Date.now() });
console.log('签名:', signature);

```

### 6.2 场景二：提取动态生成的 Cookie

```javascript
const { JSDOM, CookieJar } = require('jsdom');
const axios = require('axios');

async function getDynamicCookie(targetUrl) {
  // 1. 先用 axios 获取包含动态 JS 的页面
  const response = await axios.get(targetUrl);
  const html = response.data;

  const cookieJar = new CookieJar();

  // 2. 用 jsdom 执行页面 JS（JS 会自动写入 cookie）
  const dom = new JSDOM(html, {
    url: targetUrl,
    runScripts: 'dangerously',
    resources: 'usable',
    cookieJar,
  });

  // 3. 等待 JS 执行完毕（有些加密逻辑是异步的）
  await new Promise(resolve => setTimeout(resolve, 2000));

  // 4. 读取生成的 Cookie
  const cookies = cookieJar.getCookiesSync(targetUrl);
  return cookies.map(c => `${c.key}=${c.value}`).join('; ');
}

const cookieStr = await getDynamicCookie('https://example.com/');
console.log('动态Cookie:', cookieStr);

```

### 6.3 场景三：Hook 加密函数拦截参数

```javascript
const { JSDOM } = require('jsdom');

const dom = new JSDOM(`<!DOCTYPE html><html></html>`, {
  url: 'https://target-site.com/',
  runScripts: 'dangerously',
});

const { window } = dom;

// 先加载目标脚本
window.eval(require('fs').readFileSync('./target.js', 'utf-8'));

// Hook window.btoa 拦截 base64 编码
const originalBtoa = window.btoa.bind(window);
window.btoa = function(data) {
  console.log('[Hook btoa] 输入:', data);
  const result = originalBtoa(data);
  console.log('[Hook btoa] 输出:', result);
  return result;
};

// Hook CryptoJS（如果页面使用了）
if (window.CryptoJS) {
  const origEncrypt = window.CryptoJS.AES.encrypt;
  window.CryptoJS.AES.encrypt = function(data, key, options) {
    console.log('[Hook AES] 明文:', data.toString());
    console.log('[Hook AES] 密钥:', key.toString());
    return origEncrypt.apply(this, arguments);
  };
}

// 触发加密逻辑
window.doRequest('/api/data');

```

> 引用：*"A common pattern is to stash the original function, replace it with a wrapper that does whatever instrumentation you want, and then call the original so the page keeps behaving normally."* — [F5 Reverse Engineering Blog](https://www.f5.com/company/blog/reverse-engineering-by-example-flatmap-stream-payload-a)

### 6.4 场景四：处理 Webpack 打包的加密模块

```javascript
const { JSDOM } = require('jsdom');
const fs = require('fs');

const dom = new JSDOM(`<!DOCTYPE html><html></html>`, {
  url: 'https://target-site.com/',
  runScripts: 'dangerously',
});

const { window } = dom;

// 注入 webpack bundle
window.eval(fs.readFileSync('./bundle.js', 'utf-8'));

// 方式1：如果 bundle 暴露了 webpackRequire
// 某些 webpack bundle 会在 window 上暴露 __webpack_require__
if (window.__webpack_require__) {
  // 找到模块ID（通过 devtools 或逆向分析确定）
  const encryptModule = window.__webpack_require__('moduleId_123');
  console.log(encryptModule.encrypt('test_data'));
}

// 方式2：在 bundle 末尾追加导出钩子
const bundleWithHook = fs.readFileSync('./bundle.js', 'utf-8') + `
  // 在 bundle 末尾插入，拦截模块加载
  var _originalRequire = __webpack_require__;
  __webpack_require__ = function(id) {
    var mod = _originalRequire(id);
    window.__modules = window.__modules || {};
    window.__modules[id] = mod;
    return mod;
  };
`;
window.eval(bundleWithHook);

```

---

## 7\. Proxy 补环境技术

"补环境"是 JS 逆向中的核心技术：通过代理（Proxy）拦截代码对浏览器 API 的访问，从而发现代码依赖哪些环境变量，并逐一补充。

> 引用：*"nodejs 环境中没有 window 等对象，当网站采用前端加密时，需要从网站前端抠出前端加密过的代码，在 nodejs 中模拟浏览器运行时往往会出现缺少对象问题，采用 jsdom 可以很方便模拟浏览器中 js 的各种对象。"* — [知乎](https://zhuanlan.zhihu.com/p/624188717)

### 7.1 基础 Proxy 探针

```javascript
const { JSDOM } = require('jsdom');

const dom = new JSDOM(`<!DOCTYPE html><html></html>`, {
  url: 'https://target-site.com/',
  runScripts: 'dangerously',
});

const { window } = dom;

// 创建探针 Proxy，记录所有属性访问
function createProbe(target, name = 'window') {
  return new Proxy(target, {
    get(obj, prop) {
      const value = obj[prop];
      // 过滤掉常见的噪音属性
      if (!['Symbol', 'toString', 'valueOf', 'then'].includes(String(prop))) {
        console.log(`[GET] ${name}.${String(prop)} =>`, typeof value);
      }
      // 如果值是对象，递归代理
      if (value && typeof value === 'object' && !value.__probed) {
        return createProbe(value, `${name}.${String(prop)}`);
      }
      return value;
    },
    set(obj, prop, value) {
      console.log(`[SET] ${name}.${String(prop)} =`, value);
      obj[prop] = value;
      return true;
    },
    has(obj, prop) {
      console.log(`[IN] ${String(prop)} in ${name}`);
      return prop in obj;
    }
  });
}

// 用探针代理 window
const probedWindow = createProbe(window);

// 在探针环境中执行加密脚本
const vmContext = dom.getInternalVMContext();
require('vm').runInContext(`
  // 这里放从网站扣出的代码
  var sign = getSign({a: 1});
`, vmContext);

```

### 7.2 递归深度补环境

```javascript
// 补充常见的浏览器环境缺失项
function patchEnvironment(window) {
  // 补充 navigator 属性
  Object.defineProperty(window.navigator, 'userAgent', {
    get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    configurable: true,
  });

  Object.defineProperty(window.navigator, 'platform', {
    get: () => 'Win32',
    configurable: true,
  });

  Object.defineProperty(window.navigator, 'hardwareConcurrency', {
    get: () => 8,
    configurable: true,
  });

  // 补充 screen
  Object.defineProperty(window.screen, 'width', { get: () => 1920 });
  Object.defineProperty(window.screen, 'height', { get: () => 1080 });
  Object.defineProperty(window.screen, 'colorDepth', { get: () => 24 });

  // 补充 window 尺寸
  Object.defineProperty(window, 'innerWidth', { get: () => 1920 });
  Object.defineProperty(window, 'innerHeight', { get: () => 1080 });

  // 补充 performance.now()（部分网站用于时间戳）
  if (!window.performance) {
    window.performance = { now: () => Date.now() };
  }

  // 补充 crypto.getRandomValues（部分加密库依赖）
  if (!window.crypto) {
    const nodeCrypto = require('crypto');
    window.crypto = {
      getRandomValues: (arr) => {
        const bytes = nodeCrypto.randomBytes(arr.length);
        arr.set(bytes);
        return arr;
      },
      subtle: {},
    };
  }
}

```

### 7.3 使用 vm2 隔离执行（更安全）

```javascript
// 注意：vm2 已停止维护，了解替代方案（isolated-vm）
const { VM } = require('vm2');
const { JSDOM } = require('jsdom');

const dom = new JSDOM(`<!DOCTYPE html><html></html>`, {
  url: 'https://target-site.com/',
  runScripts: 'outside-only',
});

const vm = new VM({
  sandbox: dom.getInternalVMContext(),
  eval: false,
  wasm: false,
});

// 在隔离环境中执行
vm.run(require('fs').readFileSync('./encrypt.js', 'utf-8'));

```

---

## 8\. 反检测与指纹伪造

### 8.1 WebDriver 检测绕过

```javascript
// 网站常检测 navigator.webdriver 来识别自动化工具
Object.defineProperty(dom.window.navigator, 'webdriver', {
  get: () => undefined,  // 正常浏览器返回 undefined，selenium/puppeteer 返回 true
  configurable: true,
});

```

### 8.2 伪造 Canvas 指纹

```javascript
// 安装 canvas 包以支持 Canvas API
// npm install canvas
const { createCanvas } = require('canvas');

// jsdom 中 Canvas 默认不工作，需要挂载 node-canvas 实现
const dom = new JSDOM(`<html></html>`, {
  url: 'https://example.com/',
  runScripts: 'dangerously',
  resources: 'usable',
});

// 在 window 中注入 Canvas 构造器
dom.window.HTMLCanvasElement.prototype.getContext = function(type) {
  if (type === '2d') {
    return createCanvas(300, 150).getContext('2d');
  }
  return null;
};

```

### 8.3 伪造 WebGL 信息

```javascript
// 补充 WebGL 渲染器信息（部分网站通过 WebGL 采集指纹）
const patchWebGL = (window) => {
  const getParameter = WebGLRenderingContext.prototype.getParameter;
  WebGLRenderingContext.prototype.getParameter = function(parameter) {
    // UNMASKED_VENDOR_WEBGL
    if (parameter === 37445) return 'Intel Inc.';
    // UNMASKED_RENDERER_WEBGL
    if (parameter === 37446) return 'Intel Iris OpenGL Engine';
    return getParameter.apply(this, arguments);
  };
};

```

### 8.4 时区与语言设置

```javascript
// 伪造时区（部分网站通过 Intl API 检测）
const dom = new JSDOM(`<html></html>`, {
  url: 'https://example.com/',
  runScripts: 'dangerously',
});

// 修改 Intl.DateTimeFormat 返回的时区
const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
dom.window.Intl.DateTimeFormat.prototype.resolvedOptions = function() {
  const options = originalResolvedOptions.call(this);
  return { ...options, timeZone: 'Asia/Shanghai' };
};

// 设置 navigator.language
Object.defineProperty(dom.window.navigator, 'language', {
  get: () => 'zh-CN',
});
Object.defineProperty(dom.window.navigator, 'languages', {
  get: () => ['zh-CN', 'zh', 'en'],
});

```

---

## 9\. 常见坑与解决方案

### 坑1：`this` 上下文丢失

**问题**：从网站扣出含 `this` 的代码，直接调用时 `this` 变成 `undefined`。

```javascript
// 错误方式
const encrypt = window.eval(`(${targetFunction})`);
encrypt(); // this 可能不正确

// 正确方式：绑定上下文
const encrypt = window.eval(`(${targetFunction})`).bind(window);
encrypt();

// 或者在 window 上挂载后调用
window.eval(`window._encrypt = ${targetFunction}`);
window._encrypt();

```

### 坑2：`url` 选项必须设置

**问题**：不设置 `url` 时，`location`、Cookie 等很多功能失效，且某些脚本会检测当前域名。

```javascript
// 错误：不设置 url
const dom = new JSDOM(html, { runScripts: 'dangerously' });

// 正确：设置与目标站一致的 url
const dom = new JSDOM(html, {
  url: 'https://www.target-site.com/',
  runScripts: 'dangerously',
});

```

### 坑3：异步脚本未等待

**问题**：`setTimeout`、`Promise` 等异步加密逻辑还没执行完就读取结果。

```javascript
// 错误：立即读取
const dom = new JSDOM(html, { runScripts: 'dangerously' });
console.log(dom.window.encryptedToken); // undefined，JS还没跑完

// 正确：等待事件或轮询
await new Promise((resolve) => {
  dom.window.addEventListener('load', resolve);
  // 或者固定等待
  setTimeout(resolve, 3000);
});
console.log(dom.window.encryptedToken);

```

### 坑4：`Cannot redefine property: location`

**问题**：试图覆盖 `window.location` 时报错。

```javascript
// 某些属性无法直接赋值，需要用 Object.defineProperty
// 但 location 在 jsdom 中是受保护的，使用 url 选项代替
const dom = new JSDOM(html, {
  url: 'https://target.com/path?query=value', // 在这里设置完整 URL
  runScripts: 'dangerously',
});

```

### 坑5：外部脚本不加载

**问题**：页面依赖外部 `<script src="...">` 但没有加载。

```javascript
// 需要同时设置 url 和 resources: 'usable'
const dom = new JSDOM(html, {
  url: 'https://target-site.com/',
  resources: 'usable',  // 允许加载外部资源
  runScripts: 'dangerously',
});

// 等待资源加载完成
await new Promise(resolve => {
  dom.window.addEventListener('load', resolve);
});

```

---

## 10\. 推荐方法与工具链

### 10.1 推荐工具组合

| 工具                     | 用途                           |
| ---------------------- | ---------------------------- |
| jsdom                  | 浏览器环境模拟，执行加密 JS              |
| axios / node-fetch     | HTTP 请求，获取页面和接口数据            |
| tough-cookie           | Cookie 管理（jsdom 内部使用）        |
| undici                 | 底层 HTTP 客户端，支持代理（jsdom 内部使用） |
| vm (Node 内置)           | 安全沙箱，隔离执行脚本                  |
| canvas                 | 为 jsdom 提供 Canvas API 实现     |
| prettier / js-beautify | 格式化混淆代码，提高可读性                |

### 10.2 推荐的 jsdom 方法

```javascript
// 1. JSDOM.fromURL() — 最方便的远程页面加载
const dom = await JSDOM.fromURL(url, options);

// 2. dom.getInternalVMContext() — 获取沙箱上下文，精确控制执行环境
const ctx = dom.getInternalVMContext();
new vm.Script(code).runInContext(ctx);

// 3. dom.serialize() — 将当前 DOM 状态序列化为 HTML 字符串（调试用）
console.log(dom.serialize());

// 4. dom.window.eval() — 在页面上下文中执行字符串代码
dom.window.eval('window.result = someEncryptFn("data")');

// 5. dom.cookieJar.getCookiesSync() — 同步获取所有 Cookie
const cookies = dom.cookieJar.getCookiesSync('https://example.com/');

// 6. virtualConsole.on('jsdomError') — 捕获 jsdom 内部错误（如 CSS 解析失败）
virtualConsole.on('jsdomError', (err) => {
  // 通常可以忽略 CSS 解析错误
  if (!err.message.includes('css')) console.error(err);
});

```

### 10.3 完整逆向工作流模板

```javascript
/**
 * JS逆向通用模板
 * 适用于：需要在 Node.js 中模拟浏览器执行加密逻辑的场景
 */
const { JSDOM, VirtualConsole, CookieJar } = require('jsdom');
const vm = require('vm');
const fs = require('fs');

async function createBrowserEnv(targetUrl, options = {}) {
  const virtualConsole = new VirtualConsole();
  // 静默 CSS 解析错误（常见且通常无害）
  virtualConsole.on('jsdomError', (err) => {
    if (!err.toString().includes('css')) console.error('[jsdomError]', err);
  });
  virtualConsole.on('error', console.error);

  const cookieJar = new CookieJar();

  // 预设 Cookie（如果需要模拟已登录状态）
  if (options.cookies) {
    for (const cookie of options.cookies) {
      cookieJar.setCookieSync(cookie, targetUrl);
    }
  }

  const dom = new JSDOM(`<!DOCTYPE html><html><head></head><body></body></html>`, {
    url: targetUrl,
    runScripts: 'dangerously',
    resources: 'usable',
    pretendToBeVisual: true,
    cookieJar,
    virtualConsole,
    ...options.jsdomOptions,
  });

  const { window } = dom;

  // 补充常见缺失的浏览器 API
  patchEnvironment(window);

  // 加载自定义脚本
  if (options.scripts) {
    for (const script of options.scripts) {
      window.eval(fs.readFileSync(script, 'utf-8'));
    }
  }

  return { dom, window, cookieJar };
}

// 使用示例
(async () => {
  const { window, cookieJar } = await createBrowserEnv(
    'https://target-site.com/',
    {
      scripts: ['./extracted-encrypt.js'],
      cookies: ['sessionid=abc123; Path=/; Domain=target-site.com'],
    }
  );

  // 等待异步初始化（如果有）
  await new Promise(r => setTimeout(r, 1000));

  // 调用加密函数
  const params = window.buildParams({ uid: 999, action: 'query' });
  console.log('加密参数:', params);

  // 获取动态生成的 Cookie
  const cookies = cookieJar.getCookiesSync('https://target-site.com/');
  console.log('动态Cookie:', cookies.map(c => `${c.key}=${c.value}`).join('; '));
})();

```

---

---

## 最佳实践

**优先用 Proxy 捕获而非逐个手动补**：在顶层用 `new Proxy({}, { get(t,k){ console.log('[ENV]', k); return undefined } })` 代替 `window`，运行目标 JS 后查看日志，得到所有访问过的属性列表，再针对性补充，效率远高于逐个猜测。

**最小化补环境**：只补目标加密函数实际访问的属性，其余属性保持 `undefined`，避免过度补充引入新的依赖，减少调试复杂度。

**`jsdom` 配合 `runScripts: 'dangerously'` 执行页面内嵌脚本**：抠出整个 `<script>` 标签内容时，`new JSDOM(html, { runScripts: 'dangerously', resources: 'usable' })` 可模拟浏览器执行内嵌脚本，比单独执行脚本片段的环境更完整。

**Navigator、Screen 等属性用 `Object.defineProperty` 设置**：直接赋值 `window.navigator = {...}` 在严格模式或某些环境下会失败；用 `Object.defineProperty(window, 'navigator', { value: {...}, writable: true })` 更安全。

**Canvas 指纹补环境用固定值**：`canvas.getContext('2d').getImageData()` 等 Canvas API 在 jsdom 中默认返回全零；若目标代码依赖 Canvas 指纹，需返回一个固定的合理值而非随机值，确保签名可复现。

---

## 常见陷阱

### 陷阱：补了属性但代码仍然报错 `TypeError: xxx is not a function`

**现象：** 补了 `window.crypto = {}` 后仍报 `window.crypto.getRandomValues is not a function`。  
**原因：** 只补了父对象，没有补方法。代码访问的是嵌套属性（`crypto.getRandomValues`），需要逐层补全。  
**解决：** 用 Proxy 拦截所有属性访问，识别所有嵌套访问路径后，将整个层级一起补全。

### 陷阱：`require('jsdom')` 在浏览器环境运行报错

**现象：** 将补环境代码移植到浏览器 Console 时报 `ReferenceError: require is not defined`。  
**原因：** jsdom 是 Node.js 专用库，只能在 Node 环境使用；浏览器中不需要 jsdom，直接使用真实 DOM。  
**解决：** 补环境代码在 Node.js 中运行；若需要在浏览器中调试，直接在 DevTools Console 里测试（已有真实 DOM 环境）。

### 陷阱：jsdom 中 `setTimeout`/`setInterval` 不执行

**现象：** 补环境后代码调用了 `setTimeout` 但回调从未执行，导致后续逻辑不触发。  
**原因：** jsdom 的事件循环与 Node.js 的事件循环是分开的，且默认不自动推进时间。  
**解决：** 改用 `jest-environment-jsdom` 的时间模拟（`jest.runAllTimers()`）；或用 `jest-fake-timers`；或直接将异步回调改为同步调用。

---

## 参见

[补环境](https://blog.vercanti.com/bu-huan-jing/)  
[RPC远程调用](https://blog.vercanti.com/rpc-yuan-cheng-diao-yong/)  
[js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)  
[Proxy对象与Hook](https://blog.vercanti.com/proxy-dui-xiang-yu-hook/)