詳解升訊威線上客服系統前端 JavaScript 指令碼加密技術(1)

曹旭升(sheng.c)發表於2022-07-05

我在業餘時間開發維護了一款免費開源的升訊威線上客服系統,也收穫了許多使用者。對我來說,只要能獲得使用者的認可,就是我最大的動力。

這段時間有幾個技術小夥伴問了我一個有意思的問題:“你的前端指令碼是怎麼加密的?”

我決定寫帖子來分享這個問題的答案。

線上客服系統訪客端:

線上客服系統客服端:


免費線上使用 & 免費私有化部署:https://kf.shengxunwei.com


視訊實拍:演示升訊威線上客服系統在網路中斷,直接禁用網路卡,拔掉網線的情況下,也不丟訊息,不出異常。
https://blog.shengxunwei.com/Home/Post/fe432a51-337c-4558-b9e8-347b58cbcd53


首先開啟客服系統官方網站:https://kf.shengxunwei.com 然後檢視嵌入的客服系統 JavaScript 檔案,接著,用瀏覽器訪問嵌入站點的 JavaScript 檔案。可以看到,檔案是加密和壓縮過的:

然後我們通過格式化工具,格式化看看:

所有的變數名都經過了混淆:

所有的函式名都經過了混淆:

那麼這是怎麼做到的呢?我打算分成幾篇不同的博文,循序漸進的解答這個問題,如何混淆前端 JavaScript 程式碼。

JavaScript 語法解析

JavaScript 是如何執行的

對於常見編譯型語言(例如:Java)來說,編譯步驟分為:詞法分析->語法分析->語義檢查->程式碼優化和位元組碼生成。

對於解釋型語言(例如 JavaScript)來說,通過詞法分析 -> 語法分析 -> 語法樹,就可以開始解釋執行了。

具體過程是這樣的:

1.詞法分析是將字元流(char stream)轉換為記號流(token stream)

NAME "AST"  
EQUALS  
NAME "is Tree"  
SEMICOLON

2.語法分析成 AST (Abstract Syntax Tree)。

3.預編譯,當JavaScript引擎解析指令碼時,它會在預編譯期對所有宣告的變數和函式進行處理!並且是先預宣告變數,再預定義函式!

4.解釋執行,在執行過程中,JavaScript 引擎是嚴格按著作用域機制(scope)來執行的,並且 JavaScript 的變數和函式作用域是在定義時決定的,而不是執行時決定的。JavaScript 中的變數作用域在函式體內有效,無塊作用域;

function func(){
    for(var i = 0; i < array.length; i++){  
      //do something here.  
    }
    //此時 i 仍然有值,及 i == array.length  
    console.log(i); // 但在 java 語言中,則無效
}

JavaScript 引擎通過作用域鏈(scope chain)把多個巢狀的作用域串連在一起,並藉助這個鏈條幫助 JavaScript 直譯器檢索變數的值。這個作用域鏈相當於一個索引表,並通過編號來儲存它們的巢狀關係。當 JavaScript 直譯器檢索變數的值,會按著這個索引編號進行快速查詢,直到找到全域性物件(global object)為止,如果沒有找到值,則傳遞一個特殊的 undefined 值。

var scope = "global";
scopeTest();
function scopeTest(){  
    console.log(scope);  
    var scope = "local";  
    console.log(scope); 
}
列印結果:undefined,local;

我們常說的 V8 是 Google 釋出的開源 JavaScript 引擎,採用 C++ 編寫。SpiderMonkey(Mozilla,基於 C)、Rhino(Mozilla,基於 Java),而 Nodejs 依賴於 V8 引擎開發,接下來的內容是 JavaScript 在 V8 引擎中的執行狀態,而類似的 JavaScript 現代引擎對於這些實現大同小異。

在本文的開頭提到了編譯型語言,解釋型語言。JavaScript 是解釋型語言且弱型別,在生成 AST 之後,就開始一邊解釋,一邊執行,但是有個弊端,當某段程式碼被多次執行時,它就有了可優化的空間(比如型別判斷優化),而不用一次次的去重複之前的解釋執行。 編譯型語言如 JAVA,可以在執行前就進行優化編譯,但是這會耗費大量的時間,顯然不適用於 Web 互動。

於是就有了,JIT(Just-in-time),JIT 是兩種模式的混合。

它是如何工作的呢:

1.在 JavaScript 引擎中增加一個監視器(也叫分析器)。監視器監控著程式碼的執行情況,記錄程式碼一共執行了多少次、如何執行的等資訊,如果同一行程式碼執行了幾次,這個程式碼段就被標記成了 “warm”,如果執行了很多次,則被標記成 “hot”。

2.(基線編譯器)如果一段程式碼變成了 “warm”,那麼 JIT 就把它送到基線編譯器去編譯,並且把編譯結果儲存起來。比如,監視器監視到了,某行、某個變數執行同樣的程式碼、使用了同樣的變數型別,那麼就會把編譯後的版本,替換這一行程式碼的執行,並且儲存。

3.(優化編譯器)如果一個程式碼段變得 “hot”,監視器會把它傳送到優化編譯器中。生成一個更快速和高效的程式碼版本出來,並且儲存。例如:迴圈加一個物件屬性時,假設它是 INT 型別,優先做 INT 型別的判斷

4.(去優化)可是對於 JavaScript 從來就沒有確定這麼一說,前 99 個物件屬性保持著 INT 型別,可能第 100 個就沒有這個屬性了,那麼這時候 JIT 會認為做了一個錯誤的假設,並且把優化程式碼丟掉,執行過程將會回到直譯器或者基線編譯器,這一過程叫做去優化。

總結

明白了基本原理之後,接下來就是如何執行混淆的過程了,

未完待續。


免費線上使用 & 免費私有化部署:https://kf.shengxunwei.com


視訊實拍:演示升訊威線上客服系統在網路中斷,直接禁用網路卡,拔掉網線的情況下,也不丟訊息,不出異常。
https://blog.shengxunwei.com/Home/Post/fe432a51-337c-4558-b9e8-347b58cbcd53


相關文章