现在 NodeJs 开发 Server 端越来越流行,如果 Server 部署在自己公司的服务器上,那么可以认为环境是相对安全的,不需要做源码保护。但是如果需要在客户方部署,又不希望自己的源码暴露的时候,这个时候就需要源码保护。一般的源码保护方式就是 js 压缩/混淆之类的操作,增加 js 代码的不可读性,或者说是增加破解难度。
本文讨论另一种使用字节码编译 nodejs 代码来保护源码的方式。
NodeJs 使用 google 的 V8 引擎进行编译,具体可以参考 https://zhuanlan.zhihu.com/p/28590489,同时,我们利用 bytenode 这个插件来辅助生成字节码文件,具体请参考 https://github.com/OsamaAbbas/bytenode。
项目构成:使用 express 创建一个项目,项目目录如下:
主要文件及说明:
bin/www:程序主入口
routes/:路由 js 文件
services/:核心业务逻辑处理的 js 文件
app.js:NodeJs Server 启动入口。
compile.js:字节码编译 js 的文件,后面会讲到。
其余的文件不重要,也不会被编译成字节码。
核心思路就是将关键的 js 代码编译成字节码,以保护我们的业务处理逻辑或者算法。
1. 安装依赖
npm install bytenode --save
2. compile.js,主要逻辑就是将项目代码拷贝到 dist 目录中,遍历 dist 下 routes 和 services 等核心 js 文件目录,使用 bytenode 插件将所有的 js 转换成 jsc 字节码文件,然后删除 js 源文件。
var bytenode = require('bytenode');
var fs = require('fs');
var path = require("path");
fs.exists('./dist', exist => {
if (exist) {
delDir('./dist');
}
fs.mkdirSync('./dist');
})
// 拷贝目录到 dist 下
fs.readdir('./', (err, files) => {
if (err) {
console.error(err);
return;
}
for (var i = 0; i < files.length; i++) {
var stat = fs.statSync('./' + files[i]);
if (stat.isFile()) {
if (files[i].indexOf('compile.js') == -1) {
fs.writeFileSync('./dist/' + files[i], fs.readFileSync('./' + files[i]));
}
} else if (stat.isDirectory() && files[i].indexOf('dist') == -1) {
createDocs('./' + files[i], './dist/' + files[i], function () {
})
} else {
}
}
compileFile()
})
function compileFile() {
// 编译 app.js 为字节码
bytenode.compileFile({
filename: './dist/app.js'
});
fs.unlinkSync('./dist/app.js');
// 编译 filters/routes/services 目录下的js文件为字节码
compileDir('./dist/filters');
compileDir('./dist/routes');
compileDir('./dist/services');
}
function compileDir(dir) {
var stat = fs.statSync(dir);
if (stat.isFile() && dir.indexOf('.js') != -1) {
// 文件,直接转换
bytenode.compileFile({
filename: dir
});
fs.unlinkSync(dir);
} else if (stat.isDirectory()) {
// 目录,列出文件列表,循环处理
var files = fs.readdirSync(dir);
for (var i = 0; i < files.length; i++) {
var file = dir + '/' + files[i];
compileDir(file);
}
} else {
}
}
//递归创建目录 同步方法
function mkdirsSync(dirname) {
if (fs.existsSync(dirname)) {
return true;
} else {
if (mkdirsSync(path.dirname(dirname))) {
console.log("mkdirsSync = " + dirname);
fs.mkdirSync(dirname);
return true;
}
}
}
function _copy(src, dist) {
var paths = fs.readdirSync(src)
paths.forEach(function (p) {
var _src = src + '/' + p;
var _dist = dist + '/' + p;
var stat = fs.statSync(_src)
if (stat.isFile()) {// 判断是文件还是目录
fs.writeFileSync(_dist, fs.readFileSync(_src));
} else if (stat.isDirectory()) {
copyDir(_src, _dist)// 当是目录是,递归复制
}
})
}
/*
* 复制目录、子目录,及其中的文件
* @param src {String} 要复制的目录
* @param dist {String} 复制到目标目录
*/
function copyDir(src, dist) {
var b = fs.existsSync(dist)
console.log("dist = " + dist)
if (!b) {
console.log("mk dist = ", dist)
mkdirsSync(dist);//创建目录
}
console.log("_copy start")
_copy(src, dist);
}
function createDocs(src, dist, callback) {
console.log("createDocs...")
copyDir(src, dist);
console.log("copyDir finish exec callback")
if (callback) {
callback();
}
}
function delDir(path) {
let files = [];
if (fs.existsSync(path)) {
files = fs.readdirSync(path);
files.forEach((file, index) => {
let curPath = path + "/" + file;
if (fs.statSync(curPath).isDirectory()) {
delDir(curPath); //递归删除文件夹
} else {
fs.unlinkSync(curPath); //删除文件
}
});
fs.rmdirSync(path);
}
}
3. 修改 bin/www 文件,在最开始 引入 bytenode
require('bytenode');
var app = require('../app');
var debug = require('debug')('esreader-server:server');
var http = require('http');
...
4. 执行指令打包编译。
node compile.js
编译完成之后, dist 下面的所有文件即可作为发布到第三方服务器上的server。
这样做完之后,项目的启动,或者使用诸如 pm2 等工具来管理 server 时,都与之前的固有做法一致,不需要特殊处理。