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
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,
});
5.2 CookieJar — Cookie 管理
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
许多网站的加密逻辑依赖浏览器全局对象(window、document、navigator 等),在 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);
6.2 场景二:提取动态生成的 Cookie
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:异步脚本未等待
问题:setTimeout、Promise 等异步加密逻辑还没执行完就读取结果。
// 错误:立即读取
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;或直接将异步回调改为同步调用。