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