+
80
-

回答

这是一个非常典型的“前端代码保护”需求。为了防止别人直接下载完整的 JS 文件,通常采用 “混淆 + 切片 + 动态加载 + 运行时重组” 的组合策略。

虽然前端代码在浏览器运行最终都会还原,但这种方法可以极大增加逆向工程的成本。

下面我将提供一个完整的 Node.js 解决方案,包含以下步骤:

原始代码准备

构建脚本 (build.js):负责混淆代码、将代码切割成多个乱码片段、生成加载器。

运行环境 (index.html):通过加载器还原并运行代码。

准备工作

首先,你需要初始化项目并安装一个核心混淆库 javascript-obfuscator。

mkdir js-protector
cd js-protector
npm init -y
npm install javascript-obfuscator --save-dev

1. 编写原始代码 (source.js)

这是你想要保护的完整业务逻辑代码。

// source.js
console.log("核心程序开始加载...");

function secretAlgorithm(a, b) {
    return a * b + 100;
}

const userData = {
    name: "Admin",
    role: "SuperUser"
};

setTimeout(() => {
    const result = secretAlgorithm(10, 20);
    console.log(`计算结果: ${result}`);
    console.log(`当前用户: ${userData.name}`);
    alert("完整代码已成功动态加载并执行!");
}, 1000);

2. 编写构建/切割脚本 (build.js)

这个 Node.js 脚本是核心。它会做以下几件事:

读取 source.js。

使用 javascript-obfuscator 进行深度混淆。

将混淆后的字符串转换成 Base64 或 Hex(增加阅读难度)。

将字符串物理切割成多个 .dat 或 .txt 文件。

生成一个用于拼接和执行的 loader.js。

// build.js
const fs = require('fs');
const path = require('path');
const JavaScriptObfuscator = require('javascript-obfuscator');

// 配置
const CONFIG = {
    inputFile: './source.js',
    outputDir: './dist',
    chunkCount: 4, // 将代码切成几份
    chunkPrefix: 'data_', // 切片文件名前缀
};

// 1. 读取源码
const sourceCode = fs.readFileSync(CONFIG.inputFile, 'utf8');

// 2. 深度混淆源码
console.log("正在混淆代码...");
const obfuscationResult = JavaScriptObfuscator.obfuscate(sourceCode, {
    compact: true,
    controlFlowFlattening: true, // 控制流扁平化(让逻辑变乱)
    deadCodeInjection: true,     // 注入死代码
    stringArray: true,
    stringArrayEncoding: ['rc4'], // 字符串加密
    splitStrings: true,
    selfDefending: true,         // 自我保护(防止格式化)
});
const obfuscatedCode = obfuscationResult.getObfuscatedCode();

// 3. 将混淆后的代码转换为 Base64 (防止传输过程中出现编码问题,同时增加一层肉眼不可读)
//    这里为了演示,我们在Base64基础上再做一个简单的异或操作或翻转,让它彻底不像代码
const encodedCode = Buffer.from(obfuscatedCode).toString('base64').split('').reverse().join('');

// 4. 切割字符串
console.log("正在切割代码...");
const totalLen = encodedCode.length;
const chunkSize = Math.ceil(totalLen / CONFIG.chunkCount);
const chunks = [];

if (!fs.existsSync(CONFIG.outputDir)) {
    fs.mkdirSync(CONFIG.outputDir);
}

for (let i = 0; i < CONFIG.chunkCount; i++) {
    const start = i * chunkSize;
    const end = start + chunkSize;
    const chunkContent = encodedCode.substring(start, end);
    const fileName = `${CONFIG.chunkPrefix}${i}.bin`; // 使用 .bin 伪装成二进制数据

    fs.writeFileSync(path.join(CONFIG.outputDir, fileName), chunkContent);
    chunks.push(fileName);
    console.log(`生成切片: ${fileName}`);
}

// 5. 生成加载器 (Loader)
// 加载器逻辑:下载所有切片 -> 排序 -> 拼接 -> 解码(反转+Base64解密) -> 执行(eval)
// 为了安全,加载器本身也应该被混淆
const loaderScript = `
(async function() {
    const files = ${JSON.stringify(chunks)};

    try {
        // 并发下载所有切片
        const promises = files.map(file => fetch(file).then(res => res.text()));
        const contents = await Promise.all(promises);

        // 拼接(因为我们在生成时是按顺序push的,Promise.all返回顺序也是一致的)
        const fullString = contents.join('');

        // 解码逻辑 (对应构建时的编码:先反转,再Base64解密)
        const reversed = fullString.split('').reverse().join('');
        const realCode = atob(reversed);

        // 动态执行 (这是核心,利用 new Function 或 eval 在全局作用域执行)
        // 使用 new Function 比 eval 稍微安全一点点,且不会影响当前闭包变量
        const run = new Function(realCode);
        run();

    } catch (e) {
        console.error("Loading failed");
    }
})();
`;

// 混淆加载器本身
const obfuscatedLoader = JavaScriptObfuscator.obfuscate(loaderScript, {
    compact: true,
    stringArray: true,
    unicodeEscapeSequence: true // 将字符串转为Unicode,增加阅读难度
}).getObfuscatedCode();

fs.writeFileSync(path.join(CONFIG.outputDir, 'loader.js'), obfuscatedLoader);
console.log("构建完成!请运行 dist/index.html");

// 生成一个简单的 index.html 用于测试
const htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dynamic Split Loading</title>
</head>
<body>
    <h1>JS动态分片加载测试</h1>
    <p>请打开控制台(F12)查看运行结果,并在Network面板观察分片加载。</p>
    <!-- 引入加载器 -->
    <script src="loader.js"></script>
</body>
</html>
`;
fs.writeFileSync(path.join(CONFIG.outputDir, 'index.html'), htmlContent);

3. 运行构建

在终端执行:

node build.js

执行后,你会看到 dist 文件夹下生成了以下文件:

data_0.bin, data_1.bin, data_2.bin, data_3.bin (你的代码碎片,里面是乱码)

loader.js (混淆后的加载逻辑)

index.html (入口文件)

4. 测试运行

由于使用了 fetch API,浏览器通常不允许直接打开本地文件 (file://) 加载 AJAX 请求。你需要启动一个简单的 HTTP 服务器。

# 如果安装了 python
cd dist
python -m http.server 8080

# 或者使用 npm 的 http-server
npx http-server ./dist

访问 http://localhost:8080。

你会看到的现象:

Network 面板:浏览器请求了 loader.js,随后并发请求了 4 个 .bin 文件。

Console 面板:输出了源码中的日志,弹出了 alert 框。

安全性

如果你打开任何一个 .bin 文件,看到的是反转过的 Base64 乱码。

如果你看 loader.js,是一堆混淆过的逻辑,很难看出它是怎么拼接文件的。

没有一个单独的请求包含完整的 source.js。

核心原理总结与进阶建议

1. 为什么这种方法有效?

网络欺骗:网络请求看到的是一堆名为 .bin 或 .png(你可以伪装后缀)的文件,且内容不是合法的 JS 语法,过滤器和爬虫容易忽略。

内存重组:完整的代码只在浏览器内存中短暂存在(字符串拼接后传给 new Function),磁盘上没有完整文件。

双重混淆:业务代码混淆了一次,加载逻辑又混淆了一次,攻击者需要先逆向加载器,知道拼接规则和解密算法,才能拿到混淆后的业务代码。

2. 如何进一步加强?

虽然上面的方法已经防住了大部分普通人,但高手可以通过 Hook new Function 或 eval 拦截最终执行的字符串。

进阶方案:

自定义加密 (XOR/AES):不要只用 Base64。在 build.js 里用一个密钥(Key)对内容进行 AES 加密。在 loader.js 里解密。虽然 Key 必须在前端(或通过接口获取),但这增加了静态分析的难度。

技巧:把 Key 拆散藏在 HTML 的 DOM 属性里,或者通过 URL 参数传递。

WebAssembly (Wasm):将解密逻辑甚至核心业务逻辑用 C/Rust 编写并编译成 Wasm。JS 只负责下载加密数据传给 Wasm,Wasm 内部解密并执行(如果是纯计算逻辑)或返回给 JS。Wasm 的逆向难度比 JS 高一个数量级。

顺序打乱:目前的切片是 0, 1, 2, 3。你可以随机生成切片文件名(如 a1z.dat, 9qq.dat),然后在 loader.js 中维护一个映射表来还原顺序。

检测调试:在 source.js 中加入检测 DevTools 是否打开的代码(如 debugger 陷阱或时间差检测),如果发现正在被调试,则故意加载错误的切片或崩溃。

警告

前端没有绝对的安全。只要代码在用户的浏览器中运行,CPU 就必须能读懂它,理论上用户就能读懂它。以上手段只能做到“防君子不防小人”以及增加破解的时间成本。重要业务逻辑(如支付验证、权限判断)必须放在后端处理。

网友回复

我知道答案,我要回答