摘要:而對(duì)于應(yīng)用越來(lái)越廣泛的而言,運(yùn)行的則是源代碼。通過(guò)查閱的相關(guān)代碼,可以發(fā)現(xiàn)字節(jié)碼的頭部保存著這些信息其中第項(xiàng)就是源代碼長(zhǎng)度。本文同時(shí)發(fā)表于作者個(gè)人博客保護(hù)項(xiàng)目的源代碼
SaaS(Software as a Service,軟件即服務(wù)),是一種通過(guò)互聯(lián)網(wǎng)提供軟件服務(wù)的模式。服務(wù)提供商會(huì)全權(quán)負(fù)責(zé)軟件服務(wù)的搭建、維護(hù)和管理,使得他們的客戶從這些繁瑣的工作中解放出來(lái)。對(duì)于許多中小型企業(yè)而言,SaaS 是采用先進(jìn)技術(shù)的最好途徑。
然而,對(duì)于大型企業(yè)而言,情況有所不同。出于產(chǎn)品定制、功能穩(wěn)定以及掌握自身數(shù)據(jù)資產(chǎn)等方面的考慮,即使成本增加,他們也更樂(lè)意把相關(guān)服務(wù)部署在企業(yè)自己的硬件設(shè)備上,也就是常說(shuō)的私有化部署。
在私有化部署的過(guò)程中,服務(wù)提供商首先要確保自己的源代碼不被泄露,否則產(chǎn)品就可以隨意復(fù)制和更改,得不償失。傳統(tǒng)的后端運(yùn)行環(huán)境,如 Java、.NET,其源代碼是經(jīng)過(guò)編譯才部署到服務(wù)器上運(yùn)行的,不存在泄露的風(fēng)險(xiǎn)。而對(duì)于應(yīng)用越來(lái)越廣泛的 Node.js 而言,運(yùn)行的則是源代碼。即使經(jīng)過(guò)壓縮混淆,也可以很大程度地還原。
本文介紹一種可用于 Node.js 端的代碼保護(hù)方案,使得 Node.js 項(xiàng)目也可以放心地進(jìn)行私有化部署。
原理當(dāng) V8 編譯 JavaScript 代碼時(shí),解析器將生成一個(gè)抽象語(yǔ)法樹(shù),進(jìn)一步生成字節(jié)碼。Node.js 有一個(gè)叫做 vm 的內(nèi)置模塊,創(chuàng)建 vm.Script 的實(shí)例時(shí),只要在構(gòu)造函數(shù)中傳入 produceCachedData 屬性,并設(shè)為 true,就可以獲取對(duì)應(yīng)代碼的字節(jié)碼。例如:
const vm = require("vm"); const CODE = "console.log("Hello world");"; // 源代碼 const script = new vm.Script(CODE, { produceCachedData: true }); const bytecodeBuffer = script.cachedData; // 字節(jié)碼
并且,這段字節(jié)碼可以脫離源代碼運(yùn)行:
const anotherScript = new vm.Script(" ".repeat(CODE.length), { cachedData: bytecodeBuffer }); anotherScript.runInThisContext(); // "Hello world"
這段代碼看起來(lái)不那么容易理解,主要體現(xiàn)在創(chuàng)建 vm.Script 實(shí)例時(shí)傳入的第一個(gè)參數(shù):
既然源代碼的字節(jié)碼已經(jīng)在 bytecodeBuffer 中,為何還要傳入第一個(gè)參數(shù)?
為何傳入與源代碼長(zhǎng)度相同的空格?
首先,創(chuàng)建 vm.Script 實(shí)例時(shí),V8 會(huì)檢查字節(jié)碼(cachedData)是否與源代碼(第一個(gè)參數(shù)傳入的代碼)匹配,所以第一個(gè)參數(shù)不能省略。其次,這個(gè)檢查非常簡(jiǎn)單,它只會(huì)對(duì)比代碼長(zhǎng)度是否一致,所以只要使用與源代碼長(zhǎng)度相同的空格,就可以“欺騙”這個(gè)檢查。
細(xì)心的讀者會(huì)發(fā)現(xiàn),這樣一來(lái),其實(shí)字節(jié)碼并沒(méi)有完全脫離源代碼運(yùn)行,因?yàn)樾枰玫皆创a長(zhǎng)度這項(xiàng)數(shù)據(jù)。而實(shí)際上,還有其他方法可以解決這個(gè)問(wèn)題。試想一下,既然有源代碼長(zhǎng)度檢查,那就說(shuō)明字節(jié)碼中也必然保存著源代碼的長(zhǎng)度信息,否則就無(wú)法對(duì)比了。通過(guò)查閱 V8 的相關(guān)代碼,可以發(fā)現(xiàn)字節(jié)碼的頭部保存著這些信息:
// The data header consists of uint32_t-sized entries: // [0] magic number and (internally provided) external reference count // [1] version hash // [2] source hash // [3] cpu features // [4] flag hash
其中第 [2] 項(xiàng) source hash 就是源代碼長(zhǎng)度。但因?yàn)?Node.js 的 buffer 是 Uint8Array 類型的數(shù)組,所以 uint32 數(shù)組中的 [2],相當(dāng)于 uint8 數(shù)組中的 [8, 9, 10, 11]。
接著把上述位置的數(shù)據(jù)提取出來(lái):
const lengthBytes = bytecodeBuffer.slice(8, 12);
其結(jié)果類似于:
這是一種叫做 Little-Endian 的字節(jié)序,低位字節(jié)排放在內(nèi)存的低地址端,高位字節(jié)排放在內(nèi)存的高地址端。
firstByte + (secondByte 256) + (thirdByte 256**2) + (forthByte * 256**3)
寫(xiě)成代碼如下:
const length = lengthBytes.reduce((sum, number, power) => { return sum += number * Math.pow(256, power); }, 0); // 27
此外,還有一種更簡(jiǎn)單的方法:
const length = bytecodeBuffer.readIntLE(8, 4); // 27
綜上所述,運(yùn)行字節(jié)碼的代碼可以優(yōu)化為:
const length = bytecodeBuffer.readIntLE(8, 4); const anotherScript = new vm.Script(" ".repeat(length), { cachedData: bytecodeBuffer }); anotherScript.runInThisContext();編譯文件
講清楚原理之后,下面就嘗試編譯一個(gè)很簡(jiǎn)單的項(xiàng)目,目錄結(jié)構(gòu)如下:
src/
lib.js
index.js
dist/
compile.js
src 目錄內(nèi)的兩個(gè)文件為源代碼,內(nèi)容分別為:
// lib.js console.log("I am lib"); exports.add = function(a, b) { return a + b; };
// index.js console.log("I am index"); const lib = require("./lib"); console.log(lib.add(1, 2));
dist 目錄用于放置編譯后的代碼。compile.js 即為執(zhí)行編譯操作的文件,其流程也非常簡(jiǎn)單,讀取源文件內(nèi)容,編譯為字節(jié)碼后保存為文件(dist/*.jsc):
const path = require("path"); const fs = require("fs"); const vm = require("vm"); const glob = require("glob"); // 第三方依賴包 const srcPath = path.resolve(__dirname, "./src"); const destPath = path.resolve(__dirname, "./dist"); glob.sync("**/*.js", { cwd: srcPath }).forEach((filePath) => { const fullPath = path.join(srcPath, filePath); const code = fs.readFileSync(fullPath, "utf8"); const script = new vm.Script(code, { produceCachedData: true }); fs.writeFileSync( path.join(destPath, filePath).replace(/.js$/, ".jsc"), script.cachedData ); });
運(yùn)行 node compile 后,就可以在 dist 目錄內(nèi)生成源代碼對(duì)應(yīng)的字節(jié)碼文件,接下來(lái)就是運(yùn)行字節(jié)碼文件。然而,直接執(zhí)行 node index.jsc 是無(wú)法運(yùn)行的,因?yàn)?Node.js 在默認(rèn)情況下會(huì)把目標(biāo)文件當(dāng)做 JavaScript 源代碼來(lái)執(zhí)行。
此時(shí),就需要對(duì) jsc 文件使用特殊的加載邏輯。在 dist 目錄內(nèi)新建文件 main.js,內(nèi)容如下:
const Module = require("module"); const path = require("path"); const fs = require("fs"); const vm = require("vm"); // 加載 jsc 文件的擴(kuò)展 Module._extensions[".jsc"] = function(module, filename) { const bytecodeBuffer = fs.readFileSync(filename); const length = bytecodeBuffer.readIntLE(8, 4); const script = new vm.Script(" ".repeat(length), { cachedData: bytecodeBuffer }); script.runInThisContext(); }; // 調(diào)用字節(jié)碼文件 require("./index");
執(zhí)行 node dist/main,雖然 jsc 文件可以加載進(jìn)來(lái)了,但是就出現(xiàn)了另一段異常信息:
ReferenceError: require is not defined
這是個(gè)奇怪的問(wèn)題,在 Node.js 中,require 是個(gè)很基礎(chǔ)的函數(shù),怎么會(huì)未定義呢?原來(lái),Node.js 在編譯 js 文件的過(guò)程中會(huì)對(duì)其內(nèi)容進(jìn)行包裝。以 index.js 為例,包裝后的代碼如下:
(function (exports, require, module, __filename, __dirname) { console.log("I am index"); const lib = require("./lib"); console.log(lib.add(1, 2)); });
包裝這個(gè)操作并不在編譯字節(jié)碼這個(gè)步驟里面,而是在之前執(zhí)行。所以,要在 compile.js 補(bǔ)上包裝(Module.wrap)操作:
const script = new vm.Script(Module.wrap(code), { produceCachedData: true });
加上包裝之后,script.runInThisContext 就會(huì)返回一個(gè)函數(shù),執(zhí)行這個(gè)函數(shù)才能運(yùn)行模塊,修改代碼如下:
Module._extensions[".jsc"] = function(module, filename) { // 省略 N 行代碼 const compiledWrapper = script.runInThisContext(); return compiledWrapper.apply(module.exports, [ module.exports, id => module.require(id), module, filename, path.dirname(filename), process, global ]); };
再次執(zhí)行 node dist/main.js,出現(xiàn)了另一條錯(cuò)誤信息:
SyntaxError: Unexpected end of input
這是一個(gè)讓人一臉懵逼,不知道從何查起的錯(cuò)誤。但是,仔細(xì)觀察控制臺(tái)又可以發(fā)現(xiàn),在錯(cuò)誤信息之前,兩條日志已經(jīng)打印出來(lái)了:
I am index
I am lib
由此可見(jiàn),錯(cuò)誤信息是執(zhí)行 lib.add 時(shí)產(chǎn)生的。所以,結(jié)論就是,函數(shù)以外的邏輯可以正常執(zhí)行,函數(shù)內(nèi)部的邏輯執(zhí)行失敗。
回想 V8 編譯的流程。它解析 JavaScript 代碼的過(guò)程中,Toplevel 部分會(huì)被解釋器完全解析,生成抽象語(yǔ)法樹(shù)以及字節(jié)碼。Non Toplevel 部分僅僅被預(yù)解析(語(yǔ)法檢查),不會(huì)生成語(yǔ)法樹(shù),更不會(huì)生成字節(jié)碼。Non Toplevel 部分,即函數(shù)體部分,只有在函數(shù)被調(diào)用的時(shí)候才會(huì)被編譯。
所以問(wèn)題也就一目了然了:函數(shù)體沒(méi)有編譯成字節(jié)碼。幸好,這種行為也是可以更改的:
const v8 = require("v8"); v8.setFlagsFromString("--no-lazy");
設(shè)置了 no-lazy 標(biāo)志后再執(zhí)行 node compile 進(jìn)行編譯,函數(shù)體也可以被完全解析了。最終 compile.js 代碼如下:
const path = require("path"); const fs = require("fs"); const vm = require("vm"); const Module = require("module"); const glob = require("glob"); const v8 = require("v8"); v8.setFlagsFromString("--no-lazy"); const srcPath = path.resolve(__dirname, "./src"); const destPath = path.resolve(__dirname, "./dist"); glob.sync("**/*.js", { cwd: srcPath }).forEach((filePath) => { const fullPath = path.join(srcPath, filePath); const code = fs.readFileSync(fullPath, "utf8"); const script = new vm.Script(Module.wrap(code), { produceCachedData: true }); fs.writeFileSync( path.join(destPath, filePath).replace(/.js$/, ".jsc"), script.cachedData ); });
dist/main.js 代碼如下:
const Module = require("module"); const path = require("path"); const fs = require("fs"); const vm = require("vm"); const v8 = require("v8"); v8.setFlagsFromString("--no-lazy"); Module._extensions[".jsc"] = function(module, filename) { const bytecodeBuffer = fs.readFileSync(filename); const length = bytecodeBuffer.readIntLE(8, 4); const script = new vm.Script(" ".repeat(length), { cachedData: bytecodeBuffer }); const compiledWrapper = script.runInThisContext(); return compiledWrapper.apply(module.exports, [ module.exports, id => module.require(id), module, filename, path.dirname(filename), process, global ]); }; require("./index");bytenode
實(shí)際上,如果你真的需要把 JavaScript 源代碼編譯成字節(jié)碼,并不需要自己去編寫(xiě)這么多的代碼。npm 平臺(tái)上已經(jīng)有一個(gè)叫做 bytenode 的包可以完成這些事情,并且它在細(xì)節(jié)和兼容性上做得更好。
字節(jié)碼的問(wèn)題雖然編譯成字節(jié)碼后可以保護(hù)源代碼,但字節(jié)碼也會(huì)存在一些問(wèn)題:
JavaScript 源代碼可以在任何平臺(tái)的 Node.js 環(huán)境中運(yùn)行,但字節(jié)碼是平臺(tái)相關(guān)的,在何種平臺(tái)下編譯,就只能在何種平臺(tái)下運(yùn)行(比如在 Windows 下編譯的字節(jié)碼不能在 macOS 下運(yùn)行)。
修改源代碼后要再次編譯為字節(jié)碼,較為繁瑣。對(duì)于一些如數(shù)據(jù)庫(kù)服務(wù)器地址、端口號(hào)等配置信息,建議不要編譯成字節(jié)碼,仍使用源文件運(yùn)行,方便隨時(shí)修改。
后記作為一名聰明的讀者,你必定能猜到,本文是以倒敘的方式寫(xiě)的。筆者是先使用 bytenode 完成了需求,再研究其原理。
本文同時(shí)發(fā)表于作者個(gè)人博客:《保護(hù) Node.js 項(xiàng)目的源代碼》
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/105412.html
摘要:本文轉(zhuǎn)載自眾成翻譯譯者網(wǎng)絡(luò)埋伏紀(jì)事鏈接原文本教程中將學(xué)習(xí)如何使用和實(shí)現(xiàn)一個(gè)本地身份驗(yàn)證策略。我們將有一個(gè)用戶頁(yè),一個(gè)備注頁(yè),和一些與身份驗(yàn)證相關(guān)的功能。下一步下一章主要涉及應(yīng)用程序的單元測(cè)試。你會(huì)學(xué)習(xí)單元測(cè)試測(cè)試金字塔測(cè)試替代等概念。 本文轉(zhuǎn)載自:眾成翻譯譯者:網(wǎng)絡(luò)埋伏紀(jì)事鏈接:http://www.zcfy.cc/article/1755原文:https://blog.risings...
摘要:本套課程包含兩大部分,第一部分是基礎(chǔ)部分,也是重要部分,參考官方文檔結(jié)構(gòu),針對(duì)內(nèi)容之間的關(guān)聯(lián)性和前后順序進(jìn)行合理調(diào)整。 showImg(https://segmentfault.com/img/bVbpBA0?w=1460&h=400); 講師簡(jiǎn)介: iview 核心開(kāi)發(fā)者,iview-admin 作者,百萬(wàn)級(jí)虛擬渲染表格組件 vue-bigdata-table 作者。目前就職于知名互...
摘要:本文翻譯自原文地址中文標(biāo)題保持的速度創(chuàng)建高性能的工具技術(shù)和提示快速摘要是一個(gè)非常多彩的平臺(tái),而創(chuàng)建服務(wù)就是其非常重要的能力之一。在目錄下,我們執(zhí)行譯者注現(xiàn)在的話可以使用新的形式的命令語(yǔ)法會(huì)在剖析完畢后,創(chuàng)建文件并自動(dòng)打開(kāi)瀏覽器。 pre-tips 本文翻譯自: Keeping Node.js Fast: Tools, Techniques, And Tips For Making Hi...
摘要:阿里云容器服務(wù)區(qū)塊鏈解決方案第一時(shí)間同步升級(jí),在新功能的基礎(chǔ)上,提供了彈性裸金屬服務(wù)器神龍內(nèi)置容器化集成阿里云日志服務(wù)等方面的增強(qiáng)。 摘要: 全球開(kāi)源區(qū)塊鏈領(lǐng)域影響最為廣泛的Hyperledger Fabric日前宣布了1.1版本的正式發(fā)布,帶來(lái)了一系列豐富的新功能以及在安全性、性能與擴(kuò)展性等方面的顯著提升。阿里云容器服務(wù)區(qū)塊鏈解決方案第一時(shí)間同步升級(jí),在v1.1新功能的基礎(chǔ)上,提供了...
摘要:未授權(quán)的爬蟲(chóng)抓取程序是危害原創(chuàng)內(nèi)容生態(tài)的一大元兇,因此要保護(hù)網(wǎng)站的內(nèi)容,首先就要考慮如何反爬蟲(chóng)。反爬蟲(chóng)的銀彈目前的反抓取機(jī)器人檢查手段,最可靠的還是驗(yàn)證碼技術(shù)。機(jī)器人協(xié)議除此之外,在爬蟲(chóng)抓取技術(shù)領(lǐng)域還有一個(gè)白道的手段,叫做協(xié)議。 本文首發(fā)于我的個(gè)人博客,同步發(fā)布于SegmentFault專欄,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處,商業(yè)轉(zhuǎn)載請(qǐng)閱讀原文鏈接里的法律聲明。 web是一個(gè)開(kāi)放的平臺(tái),這也奠定了...
閱讀 3122·2021-10-13 09:39
閱讀 1937·2021-09-02 15:15
閱讀 2505·2019-08-30 15:54
閱讀 1867·2019-08-30 14:01
閱讀 2679·2019-08-29 14:13
閱讀 1500·2019-08-29 13:10
閱讀 2788·2019-08-28 18:15
閱讀 4072·2019-08-26 10:20