IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 支付宝小程序框架分析 -> 正文阅读

[移动开发]支付宝小程序框架分析

支付宝小程序框架逆向分析

本文对支付宝小程序的正向开发做了简单介绍,并从正向开发的文件类型入手,对小程序的宿主框架进行了逆向分析,包括运行机制、通信模型以及安全防护体系等内容。

代码开发

支付宝小程序开发在语法方面与传统的前端网页开发非常类似,开发者主要编写 .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.htmlindex.jsindex.worker.js就是之前我们编写的代码所编译出的js代码。其中index.worker.js是小程序所有页面的业务逻辑代码,对应着开发者编写的pageName.js中的内容;index.htmlindex.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.jsindex.htmlindex.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的操作,将nametaobao变成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),将结果展示给用户。

小程序安全防护体系

  1. 域名通信

    小程序开发者可以在后台配置允许通信的域名白名单,该白名单存在于打包后的api-permission文件中,对应内容如下图所示。白名单限制了小程序的业务代码与外部通信的能力,仅允许向白名单中的域名发送request请求。在框架代码中,会在调用到 my.request、my.uploadFile 对应的API的实现类之前对是否允许本次操作进行校验

在这里插入图片描述
在这里插入图片描述

  1. 域名加载

    支付宝小程序提供了开放组件 web-view,用于在小程序中加载H5页面或网页。使用 web-view 组件时,需要完成H5页面中所有域名地址(含静态资源地址,如图片、.js文件地址等)配置,仅允许配置于白名单中的域名加载到小程序中。

    该白名单同样存在于打包后的 api-permission文件中,对应内容如下图所示。

在这里插入图片描述

当使用 web-view 组件加载对应的H5网页时,在使用 WebView.loadUrl() 实现页面加载之前,会对当前待加载的H5域名进行白名单正则匹配安全校验,只有当校验通过时,才会允许当前页面加载并显示。

注意:此时用于加载H5的WebView是一个新的WebView实例,跟之前用于加载 index.html并不是同一个WebView实例。

  1. 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权限控制示意图如下所示
在这里插入图片描述

  1. worker 沙箱

    1. 单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的结构是不安全的

    2. 多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设定安全令牌。

    3. 多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技术揭秘 - 掘金

支付宝小程序开发文档

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-03-03 16:26:11  更:2022-03-03 16:29:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 16:27:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码