支付宝小程序框架分析
支付寶小程序框架逆向分析
本文對支付寶小程序的正向開發做了簡單介紹,并從正向開發的文件類型入手,對小程序的宿主框架進行了逆向分析,包括運行機制、通信模型以及安全防護體系等內容。
代碼開發
支付寶小程序開發在語法方面與傳統的前端網頁開發非常類似,開發者主要編寫 .axml、.acss、.js三部分文件,分別對標前端開發中的HTML、CSS、JS。
其中 .axml 的內容如下所示,AXML 是小程序框架設計的一套標簽語言,用于描述小程序頁面的結構。
<view> Hello {{name}}! </view> <button onTap="changeName"> Click me! </button>其中 .js 的內容如下所示,用于實現小程序的業務邏輯。
// 邏輯層 var initialData = {name: 'taobao', }; // 注冊一個頁面 Page({data: initialData,changeName(e) {// 改變數據this.setData({name: 'alipay',});}, });其中 .acss 的內容如下所示,ACSS 是一套樣式語言,用于描述 AXML 的組件樣式,決定 AXML 的組件的顯示效果。
view {padding-left: 10px; }小程序開發者完成代碼開發后,會提交相應代碼給平臺審核,審核通過后,便會在支付寶上架,通過搜索小程序名稱即可使用對應小程序。
代碼打包
小程序啟動時,客戶端會從CDN下載小程序離線包,這個離線包是將原項目打包后的一個 .tar 文件,存放在 /data/data/com.eg.android.AlipayGphone/files/nebulaInstallApps 目錄下。從真機上拖出來解壓后得到目錄如下
其中 index.html、index.js、index.worker.js就是之前我們編寫的代碼所編譯出的js代碼。其中index.worker.js是小程序所有頁面的業務邏輯代碼,對應著開發者編寫的pageName.js中的內容;index.html、index.js中的內容對應著acss與axml,其中axml的組件信息、層次結構同樣被會被編譯成js代碼,在運行時由這些js進行渲染。
代碼加載
開發者寫的所有代碼最終將會打包成一份 JavaScript 腳本,在小程序啟動的時候運行,在小程序結束運行時銷毀。
雙線程模型框架
開發者開發的小程序源碼打包之后主要分為兩部分,第一部分負責小程序的視圖展示,打包產物為index.js,稱之為Render部分;第二部分負責小程序的業務邏輯、視圖更新等,打包產物為index.worker.js,稱之為Worker部分。
支付寶小程序的前端框架APPX也分為Render部分,對應需要加載的文件為af-appx.min.js,以及Worker部分,對應加載的文件為af-appx.worker.min.js。前端框架主要負責準備業務代碼需要的一些對象和數據,在準備環境的時候開始加載,用于初始化環境、往Render和Worker所處的運行環境中注冊對象之類的。
Render
整個Render部分,包含index.js(開發者編寫的視圖層代碼)和af-appx.min.js(小程序前端框架代碼),均運行在WebView上(視圖線程)。
Worker
整個Worker部分,包含index.worker.js(開發者編寫的邏輯層代碼)和af-appx.worker.min.js,均運行在V8引擎上(專有的JavaScript引擎)(應用服務線程)。
代碼加載
Render
對于Render部分而言,需要加載af-appx.min.js、index.html和index.js,從實現上,是通過WebView的loadUrl()方法,該方法可以加載網頁,也可以加載字符串格式的js代碼。
Hook住WVUCWebView 類中的loadUrl函數,可以看到Render部分的業務邏輯代碼加載
觀察index.html文件,我們可以發現,af-appx.min.js通過writeln()函數動態加載進來,同時index.js通過<script>標簽引入
同時通過hook可以觀察到af-appx.min.js內容的加載,在通過writeln()函數動態加載時,會觸發WVUCWebView對應WebViewClient的shouldInterceptRequest()函數
Worker
進入 com.alibaba.ariver.v8worker.V8Worker 類,從構造函數開始梳理加載邏輯如下
逆向代碼截圖示意如下
為方便閱讀,僅將Worker部分的邏輯整理為如下內容
d("V8_Preparing"); d("V8_InitJSEngine"); d("V8_createJsiInstance"); d("V8_CreateIsolate"); ==> 創建V8 Isolate(V8中的概念,在下方做簡單介紹) d("V8_CreateJSContext"); ==> 創建Context d("V8_SetupWebAPI"); d("V8_ReadJSBridge"); d("V8_ExecuteJSBridge"); d("V8_InjectInitialParams"); d("V8_LoadAppxWorkerJS"); ==> 加載前端框架worker部分的js:https://appx/af-appx.worker.min.js d("V8_ExecuteAppxWorkerJS"); ==> 執行前端框架worker部分的js d("V8_JSBridgeReady"); ==> V8 Worker和原生APP之間的JSBridge準備就緒 d("V8_PushWorker"); d("V8_MergeJsApiCacheParams"); d("V8_InjectFullParams"); d("V8_ImportScripts_BizJS"); ==> 加載小程序業務邏輯worker部分的js:index.worker.js d("V8_WorkerReady"); ==> 至此Worker部分已經準備就緒注解:
Isolate:Isolate和操作系統中進程的概念有些類似,進程是完全相互隔離的,一個進程里有多個線程,同時各個進程之間并不互相共享資源。Isolate也是一樣,Isolate1和Isolate2擁有各自堆棧的虛擬機實例,且相互完全隔離。
Context:在V8中,一個Context就是一個執行環境,它使得可以在一個V8實例中運行相互隔離且無關的JavaScript代碼,必須為將要執行的JavaScript代碼顯式地指定一個Context。同一個Isolate中可以創建多個Context執行環境,多個Context執行環境中的JavaScript代碼互不影響。
通過Hook,可以看到V8引擎會先加載前端框架worker部分的js代碼:https://appx/af-appx.worker.min.js,然后加載小程序業務worker部分的js代碼:index.worker.js(具體JS內容均被混淆過),最后將worker的狀態置為ready狀態。
通過Hook抓取到的業務邏輯中的JS代碼和從小程序源碼文件中提取出來的代碼一致
運行機制
架構組件
小程序實際由H5應用發展而來,且H5應用仍在支付寶上進行使用。支付寶架構組件如下圖所示,移動應用如小程序和H5應用,是使用前端技術編寫的應用,開發起來方便簡單;H5容器為移動應用提供運行環境,開放 JSAPI 供移動應用使用,提供宿主APP的原生能力;支付寶底層支持提供支付寶的功能,如網絡、存儲等等。
運行機制
細化小程序和H5容器組件中的內容,框架解析圖如下圖所示。
H5容器提供兩個JS運行環境加載移動應用,并提供 JSAPI 供其調用。其中H5應用僅運行于WebView中,且運行于主進程中;小程序的兩部分Worker和Render分別運行于V8引擎和WebView中,且小程序與宿主APP運行在不同進程中,每個小程序運行在單獨進程中,每個小程序的Render和Worker也運行在不同的線程中。
線程模型
小程序框架啟動會啟動 LiteProcessActivity,該activity運行在獨立的進程中,并在其 onCreate() 函數中初始化Render和Worker(暫未發現該部分代碼?),Render線程運行于主線程中,Worker線程運行于 "worker-jsapi"線程中,相互跨線程的調用主要依靠互相持有引用。
雙棧結構(多線程)運行機制
雙棧結構,其中Render(WebView)屬于前端,負責渲染頁面;Worker屬于后端,負責執行功能。
以下圖為例,描述前后端的各自作用。Render部分負責渲染出含有一個Button的頁面,該Button綁定了 getLocation 事件,并且使用WebView組件加載該頁面,當該Button被觸發時,Render向Worker傳遞消息,getLocation事件被觸發了,需要執行相應的JS代碼,在Worker加載的業務代碼中,getLocation事件對應的是 my.getLocation()函數,于是執行該函數,該函數通過Worker與宿主APP之間的JS Bridge向下調用,使用宿主APP的原生能力獲取地理位置信息,并且將地理位置從宿主APP傳遞到Worker,再由Worker將數據傳遞回Render,交由Render進行重新渲染,將數據顯示在可視界面上。
數據綁定、前后分離的新實踐
小程序的核心是一個響應式的數據綁定系統,分為視圖層(Render)和邏輯層(Worker)。這兩層始終保持同步,只要在邏輯層修改數據,視圖層就會相應的更新。
舉例如下
<!-- 視圖層 --> <view> Hello {{name}}! </view> <button onTap="changeName"> Click me! </button> // 邏輯層 var initialData = {name: 'taobao', }; // 注冊一個頁面 Page({data: initialData,changeName(e) {// 改變數據this.setData({name: 'alipay',});}, });小程序框架會自動將邏輯層數據中的 name與視圖層的 name進行了綁定,所以在頁面一打開的時候會顯示 Hello taobao!。當用戶點擊視圖層中定義的按鈕時,視圖層會發送 changeName的事件給邏輯層,邏輯層找到對應的事件處理函數(具體涉及到Render和Worker的通信信道,將在后續章節中詳細說明)。邏輯層執行了 setData的操作,將name 從taobao變成alipay,因為該數據和視圖層已經綁定了,從而視圖層會自動改變為Hello alipay!。
小程序主要靠視圖線程(WebView)和應用服務線程(Worker)來控制管理,兩個線程同時運行。
Worker線程啟動后,會初始化小程序,小程序初始化完成時會觸發 app.onLaunch 回調,當小程序啟動時,會觸發 app.onShow 回調,然后完成App的創建。
當頁面Page初始化時,會觸發 page.onLoad 回調,頁面完成顯示時會觸發 page.onShow 回調,然后完成Page創建,此時Worker線程等待Webview線程初始化完成通知。
Webview線程初始化完成通知Worker線程,然后Worker將初始化數據(如上示例中的initialData)發送給Webview進行渲染,此時Webview線程完成第一次數據渲染。
第一次渲染完成后,Webview線程進入就緒狀態并通知Worker線程,Worker線程調用 page.onReady 函數并進入活動狀態。
Worker線程進入活動狀態后,每次數據修改將會通知Webview線程進行渲染。當切換頁面進入后臺,應用線程調用 page.onHide 函數后,進入存活狀態;頁面返回到前臺將調用 page.onShow 函數,再次進入活動狀態;當調用返回或重定向頁面后將調用 page.onUnload函數,進行頁面銷毀。
Render與Worker 通信
小程序運行在宿主APP上,因此需要小程序到宿主APP的通信信道;與此同時,由于雙棧結構的天然隔離,還需要Worker與Render之間的通信信道。總的通信模型如下圖所示
Render
-
與宿主APP通信
JS環境內將 JSAPI 的請求與參數拼接成字符串并調用 Console.log() ,容器通過攔截給 WebView 設置的對應的 WebViewClient 中的onConsoleMessage() 函數,解析字符串并完成對應API的調用實現功能。
容器執行完對應API計算得到結果后,通過調用 WebView 的 loadUrl() 函數向JS環境回傳字符串格式的JS代碼。
-
與Worker通信
Render和Worker的雙向通信是通過 WebMessageChannel 實現的,hook相應接口可以看到 Render 向 Worker 傳遞的請求調用信息
MsgFromMsgChannel: {"func":"postMessage","param":{"data":{"i":1,"p":{"a":1,"p":[[[],[],[],[],[],[],null],[0,"getNetworkType",{"currentTarget":{"dataset":{},"id":"","offsetLeft":18,"offsetTop":84,"tagName":"view"},"detail":{"clientX":71.23809814453125,"clientY":107.04762268066406,"pageX":71.23809814453125,"pageY":107.04762268066406},"target":{"dataset":{},"id":"","offsetLeft":18,"offsetTop":84,"tagName":"view","targetDataset":{}},"timeStamp":1600233550045,"type":"tap"}]]},"t":4},"msgPortId":1,"type":"messagePort","viewId":2},"msgType":"call","clientId":"16002335500450.7511516905653794","__FastPath__":1}
Worker
-
與宿主APP通信
在V8中注入的 JSBridge 會在Java側(宿主APP)注冊回調(JsApiHandler),用于響應V8中發起的請求,然后在宿主側完成相應API的功能調用
通過hook相關接口,可以看到Worker調用NativeBridge的請求調用信息
handleAsyncJsapiRequest: {"callbackId":"getNetworkType##30","handlerName":"getNetworkType","data":{}} null宿主側完成API調用后,計算得出相應信息,并回傳給Worker
通過hook相關接口,可以看到回調返回結果信息
sendJsonToWorker(MsgFromCallback): null null {"responseData":{"err_msg":"network_type:wifi","networkAvailable":true,"networkInfo":"WIFI","networkType":"wifi"},"responseId":"getNetworkType##30"} -
與Render通信
Worker 拿到相關結果后需要將數據交由 Render 進行渲染,前面已經提過,Render和Worker的雙向通信是由 WebMessageChannel 實現的
通過 hook 相關接口,可以看到Worker向Render傳遞的信息如下
tryPostMessageByMessageChannel: postMessage,2,{"callbackId":"postMessage##31","handlerName":"postMessage","data":{"data":{"i":3,"p":{"a":3,"s":"[[{\"q\":[[1,{\"hasNetworkType\":true,\"networkType\":\"WIFI\"}]],\"t\":0}],[0,[],[],null,[]]]"},"sn":1600233535779,"t":2},"type":"messagePort","msgPortId":1,"viewId":"2","pageId":"2"}},
如果將以上通信信道用更詳細的模型圖表示,如下
通信示例
舉例一次Render–>Worker–>NativeBridge–>Worker–>Render的調用示例,如下圖
對以上通信示例做詳細說明。
開發者在 index.axml 文件中部署了一個 Button 按鈕,該按鈕綁定了對應的事件為:getLocation,當用戶使用該小程序時,會看到Render部分呈現的就是一個Button按鈕,實際上是通過WebView加載了對應的頁面。
當用戶點擊該Button時,Render向V8Worker傳遞消息,告訴Worker需要執行 getLocation 事件對應的代碼(上圖中的標號1); Worker 接收到該信息后,執行事件對應的代碼,也就是調用 my.getLocation() (上圖中的標號2); 通過綁定的Java側回調(上圖中的標號3),NativeBridge開始執行本次 JSAPI 的調用(上圖中的標號4); 在API的調用過程中,會遇到很多的權限檢查(上圖中的標號5);當通過了所有的權限檢查后,API執行成功,調用宿主底層能力拿到 getLocation 的計算結果,并將該結果回傳給 V8Worker(上圖中的標號6);Worker拿到結果后,回傳給Render進行重新渲染(上圖中的標號7),將結果展示給用戶。
小程序安全防護體系
域名通信
小程序開發者可以在后臺配置允許通信的域名白名單,該白名單存在于打包后的api-permission文件中,對應內容如下圖所示。白名單限制了小程序的業務代碼與外部通信的能力,僅允許向白名單中的域名發送request請求。在框架代碼中,會在調用到 my.request、my.uploadFile 對應的API的實現類之前對是否允許本次操作進行校驗
域名加載
支付寶小程序提供了開放組件 web-view,用于在小程序中加載H5頁面或網頁。使用 web-view 組件時,需要完成H5頁面中所有域名地址(含靜態資源地址,如圖片、.js文件地址等)配置,僅允許配置于白名單中的域名加載到小程序中。
該白名單同樣存在于打包后的 api-permission文件中,對應內容如下圖所示。
當使用 web-view 組件加載對應的H5網頁時,在使用 WebView.loadUrl() 實現頁面加載之前,會對當前待加載的H5域名進行白名單正則匹配安全校驗,只有當校驗通過時,才會允許當前頁面加載并顯示。
注意:此時用于加載H5的WebView是一個新的WebView實例,跟之前用于加載 index.html并不是同一個WebView實例。
API調用能力限制
在之前的敘述中,只有Worker擁有調用 JSAPI的能力,因為Render部分加載的僅僅是 index.axml 和 index.acss的內容,這兩個文件中并不能寫入JS代碼(此處暫不講SJS的情況)。
但在上面的第二點"域名加載"中,我們提到了 web-view 開放組件,用于加載H5網頁,使用該組件加載H5內容同樣歸屬于Render的范疇。但只要在H5網頁中引入對應的 JSBridge文件:https://appx/web-view.min.js ,即可在H5中通過JavaScript調用 JSAPI。
所以針對 API 調用能力限制的安全防護就分為了兩個方面,一是針對Worker能調用API的能力限制,二是針對Render中加載外部H5能調用API的能力限制。
-
Worker
Worker能調用的 JSAPI 同樣使用白名單進行限制,存在于 api-permission文件中,如下所示。在調用到框架層對應API的實現類之前,會先判斷該小程序是否有能力調用對應API
-
Render
WebView加載的H5能調用的API能力分為3類校驗,第一類是通過校驗當前加載的H5的域名,定義其權限等級再分配API白名單;第二類是存在部分高權限小程序appid白名單,在這個白名單上的小程序中加載的H5擁有調用所有API的能力;第三類是框架中寫死的僅允許外部H5調用的API白名單
API權限控制示意圖如下所示
worker 沙箱
單V8 Context結構(存在安全問題)
如上圖所示,在V8 Worker的初期,一個小程序占用一個V8 Isolate,一個V8 Isolate只創建一個V8 Context。于是小程序的前端框架APPX的代碼appx.worker.min.js和小程序的業務代碼index.worker.js運行于同一個V8 Isolate上的同一個V8 Context上。這樣的設計就會存在JS安全性問題,業務JS代碼就可以通過拼接冒名的形式訪問到APPX注入的內部JS對象和內部JSAPI,在同一個V8 Context中,是無法隔離開業務JS代碼和APPX框架JS代碼的運行環境的。所以這種單V8 Context的結構是不安全的
多Context隔離的V8 Worker結構(解決1中的安全問題)
如上圖所示,對于同一個小程序,在同一個V8 Isolate下,分別為前端框架腳本(af-app.worker.min.js)、小程序業務腳本(index.worker.js)和小程序插件腳本(plugin/index.worker.js)創建單獨的APPX Context、Biz Context、Plugin Context,默認情況下不同的Context是不能互相訪問的,除非通過SetSecurityToken設定安全令牌。
多Isolate隔離的多線程Worker
在小程序中,對于一些異步處理的任務,可以放置于后臺Worker線程去運行,待運行結束后,再把結果返回到小程序主線程,這就是多線程Worker。
小程序Worker主線程運行于單獨的V8 Isolate上,同時,業務JS、APPX框架JS、插件JS會運行在屬于各自的V8 Context上。同時對于每一個Worker任務,都會單獨起一個Worker線程,創建單獨的V8 Isolate和V8 Context實例。每一個Worker任務和小程序主線程中的任務都是相互線程隔離的、Isolate隔離的。Isolate隔離意味著V8堆的隔離,因此Worker主線程和后臺Worker線程,是無法直接傳遞數據的。Worker主線程和后臺Worker線程想要實現數據傳遞,則需要進行序列化和反序列化。序列化即將數據從源V8堆上拷貝至C++堆上,反序列化即將數據從C++堆上拷貝至目標V8堆上。Worker主線程和后臺Worker線程通過序列化和反序列化的接口postMessage和onMessage來進行數據傳遞。
?
參考文獻
支付寶小程序V8Worker技術揭秘 - 掘金
支付寶小程序開發文檔
總結
以上是生活随笔為你收集整理的支付宝小程序框架分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python压缩视频_如何压缩视频大小?
- 下一篇: 仅需一个参数就可搞定OneProxy的V