jsdom 完全指南 — JS逆向实战

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-%

分享

官方文档:https://github.com/jsdom/jsdom
适用版本:jsdom 20.x+(2026-05-08 核实)
最后更新: 2026-03-05(经官方文档复核)
参考来源: jsdom GitHub 官方仓库 | npm 包页面 | keepnight 补环境实战 | 知乎·jsdom 补环境详解


目录

  1. 什么是 jsdom
  2. 安装与环境
  3. 基础用法
  4. 构造函数完整选项参考
  5. 高级用法
  6. JS逆向核心场景
  7. Proxy 补环境技术
  8. 反检测与指纹伪造
  9. 常见坑与解决方案
  10. 推荐方法与工具链

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


2. 安装与环境

# 使用 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 解析

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 加载页面

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 从文件加载

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

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

3.4 访问 window 对象

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. 构造函数完整选项参考

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


5. 高级用法

5.1 VirtualConsole — 捕获脚本输出

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,
});
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 — 直接注入变量

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。

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 实例):

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 网络代理配置

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

许多网站的加密逻辑依赖浏览器全局对象(windowdocumentnavigator 等),在 Node.js 中直接运行会报错。jsdom 可以提供这些对象。

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);
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 加密函数拦截参数

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

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

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 的各种对象。"知乎

7.1 基础 Proxy 探针

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 递归深度补环境

// 补充常见的浏览器环境缺失项
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 隔离执行(更安全)

// 注意: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 检测绕过

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

8.2 伪造 Canvas 指纹

// 安装 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 信息

// 补充 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 时区与语言设置

// 伪造时区(部分网站通过 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

// 错误方式
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 等很多功能失效,且某些脚本会检测当前域名。

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

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

坑3:异步脚本未等待

问题setTimeoutPromise 等异步加密逻辑还没执行完就读取结果。

// 错误:立即读取
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 时报错。

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

坑5:外部脚本不加载

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

// 需要同时设置 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 方法

// 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 完整逆向工作流模板

/**
 * 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;或直接将异步回调改为同步调用。


参见

补环境
RPC远程调用
js逆向调试技巧
Proxy对象与Hook

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog