在前端性能优化与资源保护的博弈中,代码压缩与资源伪装是两个重要的防御维度。将经过Terser工具深度压缩的JavaScript代码嵌入到普通的图片文件中,不仅可以有效减小网络请求的可见资源体积,还能在一定程度上规避爬虫对核心业务逻辑的静态抓取。这种将代码转化为图片像素的技术,通常被称为隐写术在前端领域的一种应用。本文将详细拆解如何在Node.js环境中,从零开始构建一个TerserJS2Image工具,实现从JS代码压缩到图片编码的全流程自动化。
一、理解Terser与代码混淆的底层逻辑
在将JavaScript代码转换为图片之前,首先需要对源代码进行极致的压缩与混淆。Terser是一个基于Node.js的JavaScript解析器和压缩工具,它能够解析、压缩和混淆JavaScript代码。与传统的UglifyJS相比,Terser对现代ECMAScript标准提供了更好的支持,能够安全地处理ES6及更高版本的语法。它的核心作用在于剥离代码中的空白字符、注释,缩短变量名,并删除未使用的代码块,从而将原本可读的源码转换为极简的字符序列。
这种极简的字符序列非常适合作为图片编码的数据源。因为图片的像素存储空间是有限的,如果直接将未压缩的源码写入图片,不仅会导致生成的图片体积异常庞大,还可能超出单张图片的像素容量限制。通过Terser处理后的代码,其体积大幅缩小,使得一张几百KB的PNG图片就能轻松容纳数千行业务逻辑代码。
在Node.js中调用Terser非常简单,我们可以直接通过引入terser模块,调用其提供的minify方法。这个过程不仅支持传入字符串,也支持传入多个文件路径进行批量处理。为了后续图片编码的便利,我们需要确保Terser输出的结果是一个纯字符串,不包含Source Map等额外的辅助文件。
二、Node.js操作图片像素的核心原理
将文本数据写入图片,本质上是将文本的字符编码映射到图片的像素通道中。在计算机中,每一个字符都可以通过特定的编码格式(如UTF-8)转换为一串二进制字节流。而一张数字图片由无数个像素点组成,每个像素点通常包含红(R)、绿(G)、蓝(B)三个颜色通道,有时还包含透明度通道(Alpha)。每个通道占用8位,即1个字节,取值范围在0到255之间。
这就意味着,我们可以将文本字节流中的每一个字节,直接赋值给图片像素的一个颜色通道。例如,假设我们有一段文本转换后的字节流为[104, 101, 108, 108, 111],我们可以将其分别写入五个像素的R通道中。为了不引起图片视觉上的明显变化,通常我们会将数据隐藏在最低有效位(LSB)中,但如果是为了最大化存储容量且不关心图片的视觉效果,直接替换RGB通道的值是一种更高效的方案。
在Node.js生态中,Jimp是一个纯JavaScript编写的图像处理库,无需安装任何原生依赖即可运行。它支持读取、创建和修改图片,并且能够直接在像素级别操作图片数据。我们将利用Jimp创建一张空白图片,然后遍历Terser压缩后的字符串字节流,依次将字节写入到图片的像素通道中。
三、实现TerserJS2Image的完整代码实战
现在我们将前面的理论转化为实际可运行的代码。首先需要初始化一个Node.js项目,并安装必要的依赖包。在项目根目录下执行命令安装terser和jimp。这两个包将分别负责代码的压缩处理和图片的生成操作。
npm install terser jimp
接下来,我们编写核心的转换逻辑。整个流程分为三步:首先读取本地的JavaScript源文件,使用Terser进行压缩处理;然后计算压缩后字符串的字节长度,以此推算出需要生成的图片的宽度和高度;最后创建Jimp图片实例,通过遍历字节数组,将数据写入像素并保存为PNG图片。
const fs = require('fs');
const path = require('path');
const Terser = require('terser');
const Jimp = require('jimp');
async function convertJsToImage(inputFile, outputFile) {
// 读取JS源文件
const code = fs.readFileSync(inputFile, 'utf8');
// 使用Terser压缩代码
const result = await Terser.minify(code, {
compress: true,
mangle: true,
format: {
comments: false
}
});
if (result.error) {
throw new Error('Terser压缩失败: ' + result.error.message);
}
const minifiedCode = result.code;
// 将字符串转换为Buffer字节流
const buffer = Buffer.from(minifiedCode, 'utf8');
// 计算需要的像素数量,每个像素可存储3个字节(RGB)
const pixelCount = Math.ceil(buffer.length / 3);
const width = Math.ceil(Math.sqrt(pixelCount));
const height = Math.ceil(pixelCount / width);
// 创建空白图片
const image = new Jimp(width, height, 0x00000000);
let byteIndex = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const color = { r: 0, g: 0, b: 0 };
// 依次填充R,G,B通道
if (byteIndex < buffer.length) color.r = buffer[byteIndex++];
if (byteIndex < buffer.length) color.g = buffer[byteIndex++];
if (byteIndex < buffer.length) color.b = buffer[byteIndex++];
const hexColor = Jimp.rgbaToInt(color.r, color.g, color.b, 255);
image.setPixelColor(hexColor, x, y);
}
}
// 保存图片
await image.writeAsync(outputFile);
console.log('转换成功,图片已保存至: ' + outputFile);
}
// 执行转换
convertJsToImage('./source.js', './output.png');上述代码中,我们通过Buffer.from将Terser压缩后的字符串转换为字节流。为了确保图片尺寸的合理性,我们通过平方根计算出一个大致为正方形的宽高。在像素遍历的循环中,我们依次将字节赋值给R、G、B三个通道。当字节流读取完毕后,剩余的通道将保持默认值0。这样,一段复杂的JavaScript代码就被完美地封装进了一张看似普通的PNG图片中。
四、浏览器端逆向提取与执行图片中的JS代码
将JS代码转换为图片只是完成了第一步,如果要在前端页面中正常使用这段代码,还需要编写一套逆向提取逻辑。在浏览器端,我们可以利用Canvas API来读取图片的像素数据。首先需要通过Image对象加载图片,然后将其绘制到Canvas画布上,最后调用getImageData方法获取像素数组。
获取到的像素数据是一个Uint8ClampedArray数组,每四个元素代表一个像素的R、G、B、A值。我们需要遍历这个数组,跳过Alpha通道,将R、G、B的值重新拼接成一个完整的字节流。拿到字节流后,再通过TextDecoder将其解码为原始的JavaScript字符串。
<img id="hiddenCodeImage" src="output.png" style="display:none;" />
<script>
const img = document.getElementById('hiddenCodeImage');
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 获取像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const bytes = [];
// 每隔4个字节跳过Alpha通道
for (let i = 0; i < imageData.length; i += 4) {
bytes.push(imageData[i]); // R
bytes.push(imageData[i + 1]); // G
bytes.push(imageData[i + 2]); // B
}
// 将字节数组转换为Blob并解码
const blob = new Blob([new Uint8Array(bytes)], {type: 'text/plain'});
const reader = new FileReader();
reader.onload = function() {
const jsCode = reader.result;
// 去除可能存在的空字符截断
const cleanCode = jsCode.replace(/\0/g, '');
// 动态执行还原的JS代码
const script = document.createElement('script');
script.text = cleanCode;
document.body.appendChild(script);
};
reader.readAsText(blob);
};
</script>这段HTML代码展示了完整的逆向提取过程。需要注意的是,在Node.js端写入像素时,如果字节流长度不是3的整数倍,最后一个像素的某些通道会保持为0。在浏览器端解码时,这些0会被解码为空字符,因此我们使用正则表达式将空字符替换掉,以确保JavaScript代码的语法正确性。最后,我们动态创建一个<script>标签,将还原后的代码注入其中并挂载到DOM树上,从而实现代码的执行。
这种TerserJS2Image的技术方案,不仅为前端代码保护提供了一种新颖的思路,同时也展示了二进制数据与图像处理之间相互转换的强大能力。在实际部署时,可以结合服务端接口动态生成包含特定业务逻辑的图片,进一步增强代码的防篡改和防分析能力。