整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

微信小程序架构原理

信小程序

微信小程序包含下面四种文件:

  • js
  • json 配置文件
  • wxml 小程序专用 xml 文件
  • wxss 小程序专用 css 文件
<view>
 <text class="window">{{ text }}</text>
</view> 
Page({
 data:{
 text:"这是一个页面"
 },
 onLoad:function(options){
 // 页面初始化 options为页面跳转所带来的参数
 },
 // ........
}) 

微信小程序只能通过其 mvvm 的模板语法来动态改变页面,本身 js 并不支持 BOM 和 DOM 操作。

从开发工具看微信小程序架构

在 mac 端直接解压应用 发现 app.nw 文件夹,即开发工具源码。可以知道该项目由 nw.js 编写; 在 package.json 文件下找到应用入口:app/html/index.html。入口 js 为 dist/app.js 我们可以看到整个编辑器的大致逻辑。

但我们关心的是构建过程,在 weapp 文件夹下存在 build.js 文件。没有找到有用的信息,只看到了 upload 模块,包括对大小限制,上传包命名。

为此怀疑,微信小程序本身和 RN 类似。是在服务端打包成 native 语言的。但是通过 android 边框测试发现,微信小程序根本不是 native 原生内容。

原生界面效果:

编译过程

继续在 trans 文件夹下发现了编译模板。

  • transWxmlToJs wxml 转 js
  • transWxssToCss wxss 转 css
  • transConfigToPf 模板页配置
  • transWxmlToHtml wxml 转 html
  • transManager 管理器

用到的内容:

  • 发现用到了一个模板:app.nw/app/dist/weapp/tpl/pageFrameTpl.js, app.mw/app.dist.weapp/tpl/appserviceTpl.js
  • wcc 可执行程序,wcc 用于转转 wxml 中的自定义 tag 为 virtual_dom
  • wcsc 可执行程序,用于将 wxss 转为 view 模块使用的 css 代码,使用方式为 wcsc xxx.wxss

在模板中,我们发现使用了 WAWebview.js 文件,WAService.js文件。 在 transWxmlToJs 中我们发现一段 generateFuncReady 事件的函数。对比注册该事件的函数在 WAWebview.js 中。

我们尝试使用 wcc 对input.xml 文件进行编译。

wcc -d input.xml

生成了一段脚本:

window.__wcc_version__ = 'v0.6vv_20161230_fbi'
var $gwxc
var $gaic = 
$gwx = function (path, global) {
 function _(a, b) {
 b && a.children.push(b);
 }
 ....

通过代码我们发现,调用 $gwx 函数会再生成一个有返回值的函数(前提是 path 填写正确);于是我们执行如下代码:

$gwx("input.xml")("test")

得出如下内容:

{
 "tag": "wx-page",
 "children": [
 {
 "tag": "wx-view",
 "attr": {
 "class": "section"
 },
 "children": [
 {
 "tag": "wx-input",
 "attr": {
 "autoFocus": true,
 "placeholder": "这是一个可以自动聚焦的input"
 },
 "children": []
 }
 ]
 }
 ]
} 

这应该是一个类似 Virtual dom 的对象,交给了 WAWebivew.js 来渲染,标签名为 wx-view, wx-input。

WAWebview.js

  1. 代码在最一开始提供的是兼容性工具,还有一个 WeixinJSBridge 引入。
  2. 接下来是一个 Reporter 对象,它的作用就是发送错误和性能统计数据给后台。

  1. wx 核心对象,包含了 wx 对象下的 api。但是这里的 api 数量远远少于官方的 api 文档数量。

我们可以在代码里面发现,wx 下注册的 api 最终都会调用 WeixinJSBridge 方法。这个方法应该是在打包的时候端上注入的。我们也可以在 WAServeice.js 中找到该方法的定义。

所以我们得到了一个结论,WAService.js 是编辑器用来接受 wx 方法回调的代码。

  1. wxparser 对象,提供 dom 到 wx element 对象之间的映射操作,提供元素操作管理和事件管理功能。
  2. 之后代码是对 exparser 对象的处理,包括注册 WeixinJSBridge 全局事件,Virtual dom 算法实现,样式注入等。介绍几个组件重要的内容
  • exparser.registerBehavior 注册组件基础行为,供组件继承。

  • exparser.registerElement 为各种内置组件,注册模板,行为,属性,监听器等内容

这里我们观察到,组件:wx-video, wx-canvas, wx-contact-button, wx-map, wx-textarea 等 behaviors 都含有 "wx-native" 属性。这是不是意味着,这类组件都是 native 原生实现的呢。我们打开边框检查,发现这类组件确实都是原生的组件。

综上,微信小程序的界面有部分组件使用原生方式实现的,native 组件层在 WebView 层之上。大部分还是用前端实现的,这样解释了微信小程序的一个bug。

微信官方文档:

因为 scroll-view 是前端实现,在里面使用 native 组件,这样就无法监听滚动了。

WeixinJSBridge

组件是需要数据来渲染的,查看文档我们知道发送请求的 api 为 wx.request;通过上面分析,我们知道 wx.request 实际调用的是 WeixinJSBridge。现在我们看看 WeixinJSBridge

WeixinJSBridge 真正发送处理数据请求的是这段代码;如果当前环境是 ios, 那么调用 WKWebview 的 window.webkit.messageHandlers.invokeHandler.postMessage。如果所处环境是 android 则调用 WeixinJSCore.invokeHandler (调用的时候,默认会带上当前 webviewID)。

WAService.js

在对 WeixinJSBridge.js 分析中,我们并没有发现前端的通讯功能,路由能力,数据绑定等内容。进一步查看找到了一个 WAService.js 文件。 查看 WAService.js 文件源码:

  1. 在代码最开始,跟 WAWebview.js 一样的 WeixinJSBridge 兼容模块
  2. 然后是跟 WAWebview.js 一样的 Reporter 模块。
  3. 比 WAWebview.js 中 wx 功能更为丰富 wx 接口模块。(剩余部分 wx api 都在这里)
  4. appServiceEngine 模块,提供 Page,App,GetApp 接口
  5. 为 window 对象添加 AMD 接口 require define

综上,WAService.js 主要实现的功能:

  • App( ) 小程序的入口;Page( ) 页面的入口
  • wx API;
  • 页面有的作用域,提供模块化能力
  • 数据绑定、事件分发、生命周期管理、路由管理

到这里我们得出结论,小程序的架构方案:

整个小程序由两个 webview 组成,代码分为 UI 层和逻辑层。UI 层运行在第一个 WebView 当中,执行 DOM 操作和交互事件的响应,里面是 WAWebview.js 代码及编译后的内容。逻辑层执行在(第二个webview 中)独立的 JS 引擎中(iOS:JavaScriptCore, android:X5 JS解析器;统称 JSCore;开发工具中,nwjs Chrome 内核),WAService.js 代码和业务逻辑。

当我们对 view 层进行事件操作后,会通过 WeixinJSBridge 将数据传递到 Native 系统层。Native 系统层决定是否要用 native 处理,然后丢给 逻辑层进行用户的逻辑代码处理。逻辑层处理完毕后会将数据通过 WeixinJSBridge 返给 View 层。View 渲染更新视图。

架构的讨论

微信的这种架构,对逻辑和UI进行了完全隔离,小程序逻辑和UI完全运行在2个独立的Webview里面来处理。那么这么做的好处是啥?总感觉更加麻烦了。除了小程序外,还有人采用这种架构设计么?

在网上搜索了一下,目前使用这种架构的项目还真有一个:去哪儿最新的 YIS 框架

YIS 采取了类似小程序的架构,分为逻辑层和UI层。UI 层运行在 WebView 中,而逻辑层运行在独立的 JS 引擎中。相应地,整个应用的代码,也分为两个大的部分,一部分运行在 WebView 中,一部分运行在JS引擎中。JS引擎计算DOM结构输出给WebView,WebView转发用户的点击事件给JS引擎。

该项目做法和小程序十分类似,唯一缺少的就是没有 native 的组件吧。然而官方文档上也没有任何介绍,为什么要这么做,只是说更流畅了。

一些看法

传统 web 页面显示需要经历一下几个步骤:

  1. webview 初始化
  2. 加载 HTML, CSS, JS
  3. 编译 JS
  4. Render 计算
  5. DOM Path

而利用小程序架构后,我们就可以将上述过程拆解成两部分并行执行: webview 部分:

  1. webview 初始化
  2. 加载 HTML,CSS, JS (经过拆分后,体积大幅度减小)
  3. 编译 JS
  4. 等待页面需要的数据
  5. 反序列化数据
  6. 执行 Patch
  7. 渲染页面
  8. 等待更多消息

jscore 部分:

  1. 初始化
  2. 加载框架 js 代码
  3. 编译 js
  4. 加载业务逻辑 js 代码
  5. 编译 js
  6. 计算首屏虚拟 DOM 结构
  7. 序列化数据,传输
  8. 等待 webview 消息,或者 Native 消息

这样渲染进程和逻辑进程分离,并行处理:加速首屏渲染速度;避免单线程模型下,js 运算时间过长,UI 出现卡顿。 完全采用数据驱动的方式,不能直接操作 DOM,利用定制开发规范的方式避免低质量的代码的出现。

当然这种架构方案也有一定的缺点:

  1. 不能灵活操作 DOM,无法实现较为复杂的效果
  2. 部分和 NA 相关的视图有使用限制,如微信的 scrollView 内不能有 textarea。
  3. 页面大小、打开页面数量都受到限制
  4. 需要单独开发适配,不能复用现有代码资源。
  5. 在 jscore 中JS 体积比较大的情况下,其初始化时间会产生影响。
  6. 传输数据中,序列化和反序列化耗时需要考虑

希望本文能帮助到您!

点赞+转发,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓-_-)

关注 {我},享受文章首发体验!

每周重点攻克一个前端技术难点。更多精彩前端内容私信 我 回复“教程”

原文链接:http://eux.baidu.com/blog/fe/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%9E%B6%E6%9E%84%E5%8E%9F%E7%90%86

作者:田光宇

者 | 伍杏玲

出品 | CSDN(ID:CSDNnews)

在今年 Stack Overflow 的开发者调查报告里,我们发现一项有趣的数据:调查的 9 万名开发者中,70.8% 的开发者早在 18 岁前便写下第一行代码。

结合今年教育部新公布的《2019年教育信息化和网络安全工作要点》,推广编程教育,并对 2 万名中小学生信息素养评测。我们可以看到,全民编程时代即将来临,愈来愈多的小程序员立志加入到 IT 队伍中来:

“未来我想当个程序员,不怕掉头发~”

“虽然我没有获得很多荣誉,也没有那位小哥哥长得帅,但我的头发更加浓密,具有更多的发展空间!”

“这(代码)也太简单了吧!”

在刚过去的暑假里,100 多位平均年龄只有 13 岁的“小小程序员”参加微信主办的 2019 全国青少年微信小程序编程创意营,并自信地发出以上的宣言。

在首届微信小程序编程创意营中,这些“小小程序员”们的创意无限,“编程力”十足:

我 11 岁,4 年编程经验

创意营里年纪最小的是赖宥诚,别看他只有 11 岁,却已是具有 4 年编程经验的“老司机”。

他 7 岁时,接过爸爸丢过来的编程书,开始踏上编程之路。随后他自学 Python、HTML 等语言,并用 Scratch 编写一些小游戏和学习乐高机器人编程。在 2018 年的 WRO 比赛(国际奥林匹克机器人大赛),他所在的团队获得了季军。

但对于微信小程序,赖宥诚还是从此次的创意营首次“触电”,他零基础学习微信小程序开发,不到一周便写出自己的第一个小程序——LYC计算器小程序

谈到设计初衷,赖宥诚观察到同学们做数学题时经常记不住计算公式,于是他想设计出一款方便计算的小程序来代替麻烦的人工计算。

这个计算机小程序包含有面积、体积、表面结合勾股定律、二元一次方程组、一元二次方程、求最大公因数和最小公倍数等囊括中小学的数学公式及方程,可谓是公式“百宝箱”。

LYC计算器小程序

在开发的过程中,赖宥诚也曾陷入思维惯性:在求最大公因数时,赖宥诚一开始想的算法是:找出两个数的因数全找出来,再一个个比对。但他很快发现这是行不通的,在数据很大的时候这个算法很慢,因数太多了,最后他找到欧几里德算法来解决。

他和其他程序员一样,也有“不小心”删掉一个模块又重写新代码的痛苦经历。

毕竟赖宥诚是单枪匹马开发这小程序的,负责逻辑设计、UI、发布整个小程序的所有工作。

笔者现场发现,在创意营众多的小小程序员中,赖宥诚年纪虽小,可表现沉稳、表达清晰、动手能力强,其他同学很自然地围绕在他身边观看其编程操作。

在大家开始学习开发一款小游戏时,有些同学还在问老师,赖宥诚已打开编辑器,慢悠悠地开始动手设计了。

赖宥诚说自己很喜欢编程,平常学习任务重,他会在周末自发地学习编程。谈及未来,他表示“未来我会继续努力学习,成为一个厉害的程序员,设计很多优秀的软件!”

码二代、“历史迷”,打造学习历史小程序

13 岁的张哲涵是一个“码二代”:父母均从事编程工作,耳濡目染下,他在小学四五年级开始学习编程,有 C++ 编程背景。张哲涵还是个“历史迷”,喜欢看《上下五千年》《明朝那些事》《中国通史》等历史书籍。

从 2021 年开始历史将列入广州中考,历史将成初中生们的“必修课”。传统学习历史的方法是枯燥地死记硬背,如何能帮忙同学们更灵活、有趣地学习历史呢?

对编程和历史均热衷的张哲涵和其他两位同学一同开发了一款学习历史小程序——“知史乎”。

这个小程序中功能很丰富:设有 “中国古代朝代排序游戏”“教材知识点梳理”、“易错题和难题答题游戏”、“答题方法分析”、“历史笑话”等功能,集知识干货与趣味学习于一体。

知史乎小程序

需要开发这么多的功能,他们在开发过程中遇到不少问题,处理方式也很“程序员”:

1、需学习新的编程语言,JavaScript、WXML、WXSS,他们参考微信开发文档,边做边学。

2、在开发过程中,遇到“很多很多”Bug,耐心地一个个Debug。

3、在上传历史资料时,需要上传的文字和图片太多了,文件太大会导致上传卡机。最终利用小程序后台的分包功能,成功将初中历史知识点、答题方法解析等众多知识点全部上传成功。

目前,“知史乎”小程序仅包括七年级的历史,谈及未来,张哲涵表示它“不是昙花一现的产品”,后续将会加入更多的历史课程内容。

看到这,可能很多程序员会有疑问,“这些小程序员是如何做到的?”

三小时开发小程序,他为何直呼“太简单”?

8 月 29 日,笔者在腾讯滨海大厦观看小小程序员的一节编程课:在三个小时内开发一款美食地图小程序

在辅助代码和辅导员的指导下,小小程序员们按照操作步骤完成环境部署、查询、刷新、列表、查看、新增等功能体验,并调试与运行。

有趣的是,有一位小同学边调试程序边直呼:“太简单了!”

小程序员现场开发

可能会有人发出疑问:三个小时可能连服务都还没部署好?为什么他们能这么快能开发出一个小程序

为了让大家专注前端业务功能,这次课程借助小程序·云开发来负责后端处理。云开发是由微信团队和腾讯云联合打造的 Serverless 云服务,是一个支持小程序、Web 等多端的应用开发平台。提供云数据库、云存储、云函数、日志和监控等开发运维能力。开发者可使用云开发平台,调用小程序的开放服务,来提升开发效率,快速试错和落地产品。

云开发还提供一个便捷的技术是“免鉴权”。为了保护用户数据安全,小程序开发者每次调用小程序开放平台的能力前都需要微信鉴权获取,而原有的鉴权流程较复杂,有些场景下还需要额外部署缓存服务,对开发者来说成本较高。

使用云开发后,只需要调用一个接口,就可以获取用户登录态(OpenID),节省开发者的时间,提高效率。

小程序所见即所得,学生快速上手

在去年 11 月第五届世界互联网大会上,小程序获选“世界互联网领先科技成果”。马化腾曾表示,小程序打破了过去受限的开发环境,构建出一个新的开发环境和开发者生态,为“跨系统开发”这个世界难题给出了中国的解决方案。

小程序简单、易上手,这是微信从平均年龄只有 13 岁的初中生开展创意营的前提。

腾讯集团市场与公关部副总经理张军表示, 过去我们的语言体系太复杂,没有一个高层次的数学知识,非常好的逻辑体系,你可能都没有办法真正的编程,即使你学 C++,刚学的时候也只是浅显地知道语言,能够做模拟式的程序运营,但不能跟自己的真实生活联系起来。

小程序所见即所得,编完之后马上被身边的人使用,给中小学生的成就感很大。

张军还表示:我希望小学生、初中生能在自己的正常学习之外能有一些兴趣,如果这些兴趣还能反哺到对学习,这是很好的促进模式。

所以我们希望创造这样的环境,这里面有很多新的编程模式、编程体验,都能让他们体验,这对他们来讲是最大的乐趣。

在培养小程序员的编程能力时,为什么此次采用创意营的方式来让学生学习小程序编程,而不是采用传统方式,大家坐在课堂上,看着教材,听老师讲课?

广大附中教育集团副理事长、广州大学附属中学副校长李卫表示:

因为(计算机)教材已经远远落后时代步伐了。

比如(小程序)编程,这样的语言很好,由浅到深,(让学生)先接触,先入门,再慢慢深入,这是很好的(学习方式)。所以我们现在也提出信息课的改革,结合现在的需要开设课程。

从本次微信小程序编程创意营中,我们看到这些小小程序员对编程的热爱与创意,相信在不久的将来,会为 IT 界注入年轻新力量。

与此同时,作为专业程序员的我们亦有危机感,不断学习。因为时代不断在变化,学习编程的门槛将越来越低,正如 AI 界的大牛吴恩达所说:

“现在人机交流正在变得越来越重要,可以预见,编程能力将会成为未来最深层次的人机交流的基础。所以我不认同那些认为世界上只需要几百万程序员的观点,在我看来,(几乎)每个人都应该学习编程,就像每个人都应该学习阅读和写作一样。”

【End】

初在研究对移动网络传输进行功耗优化,在一次意外的监听网络传输包中截获了微信小程序的请求包,借此来窥探当下前端代码安全。

0x01 小程序分析

小程序包结构

SegmentNameLengthRemark
HeaderFirstMark1 byte0xBE 固定值
Edition4 bytes0 -> 微信分发到客户端 1 -> 开发者上传到微信后台
IndexInfoLength4 bytes索引段的长度
BodyInfoLength4 bytes数据段的长度
LastMark1 byte0xED 固定值
FileCount4 bytes文件数量
IndexNameLength4 bytes文件名长度
NameNameLength bytes文件名,长度为NameLength
FileOffset4 bytes文件在数据段位置
FileSize4 bytes文件大小
LOOP......


DataFiles Package......

包结构非常清晰,分为三个部分:

  1. 头信息,包含一些包的标识,版本定义等,包含了三个冗余字段 --- 索引段和数据段的长度应该是用于做校验,但实质上没有用(设计者觉得需要设计一些冗余字段来确保设计的完整性,防止解析的时候溢出,但实际工程实践中并没有起到相应的作用),文件数量应该是用于简化包解析过程,实际上知道了索引段长度或数据段长度中任何一个皆可推算出文件数量。

  2. 索引段,包含文件的元信息 --- 文件名以及文件位置(通过FileOffset和FileSize定位数据段中的文件)。如果从精简包的大小的角度来看,FileOffset和FileSize只需一个存在即可,但是这样解析包的难度就大大增加了,还是以工程实践为主。

  3. 数据段,将所有的文件罗列在一起。

由此可见,数据完全没有经过压缩或者加密,连包的签名信息也没有。这导致只能在制品流程上进行严格控制,例如在开发者上传代码过程中需要授信,必须经过审查,也一定得由授信平台进行代码分发等。这些都无关风月,毕竟App Store就是这种模式,但是......

如何拆解这种自定义文件格式呢?

  1. 对多个相同格式的文件进行对比,对大体结构有宏观的感觉,很容易发现一些固定的字段以及一些结构的长度。对于像小程序这种有软件本体的例子,还可以通过微量修改来观察文件的变化来找到文件结构和意义。

  2. 观察特殊形式,首先英文的字符串是很明显的,一般hex编辑器都自带字符串化窗口,如果发现常见的字符串,就可以继续去寻找字符串的边界,字符串在二进制文件里有两种储存方式:一种是不记录字符串的长度,读取字符串到0x00位置,另一种一定在某一个地方储存了这个字符串的长度,因此一旦得知了该字符串的内容,搜索该长度字段即可获取更多的信息。其次一些文件头也非常显眼,例如PNG[1]、ZIP[2]等通用标准文件格式都有固定的文件头,在小程序的自定义格式中很容易发现一些png、jpg等资源的文件头,因此可以定位数据区的位置。

  3. 对特定区域的二进制进行推理猜测,一般来说二进制文件里需要储存大量的offset和size的数据作为数据段的索引。offset相当于一个指针 - 索引文件在数据段的位置,工程实践中,大部分储存了offset的地方也会储存size字段,毕竟在解析文件的时候会方便很多,也可以防止校验数据出现指针越界。因此,一旦确认了文件中的数据段,就可以通过它的位置(offset)和大小(size)的实际数据进行搜索,逆向找到指向它的数据位置,并且继续逆向直到解析完整的文件。另外,如果要考虑设计的完备性,需要在二进制文件中加入一些冗余字段进行校验或者纠错,例如CheckSum、CRC32、Alder32、MD5、ECC等,这些通过hex编辑器很容易计算并发现。小程序中FileCount的字段,这完全是为了工程实践考虑的,在小程序中并没有出现这类的计算值,这是可能是因为小程序为了简单设计考虑,一旦发现包体被篡改或损坏就直接丢弃。

其实拆解小程序这种格式并不需要花费特别多的精力,因为其格式比较简单,而且从下图流程上来说,后缀为wx的二进制格式很可能与wxapkg格式是同源的。

开发者工具上传服务器分发原始代码后缀为wx代码包处理为wxapkg格式包体客户端

从开发者工具的代码中的pack.js很容易发现一些对wx格式封装的痕迹,只不过其中unpack.js的代码被隐去了。通过实际的分析发现(wxapkg文件可以通过截获网络包请求获得或者在本地的微信appbrand目录下可以发现),wxapkg格式就是将wx格式进行了转化:Wxml -> Html、 Wxml -> JS、Wxss -> Css,其二进制格式跟后缀名为wx二进制格式完全一致。我写了两个版本的解析二进制包的代码(Javascript版本传送门,python版本传送门),其实非常简单,根据小程序包结构一步一步解析,基本上没啥难度。但如果要将Html -> Wxml, JS -> Wxml, Css -> Wxss进行还原,其中JS -> Wxml的过程中需要将if语句转变成wx-if标签、for语句转化成wx-for标签有点麻烦,需要对解析包后的page-frame.html中 JS 代码进行修改,修改细节太多就不再详说了,总之微信小程序的代码没有经过额外的保护措施,比较容易进行还原。

(PS:暴露一下微信小程序未公开的API,openUrl- 在小程序中打开外部网页;getGroupInfo- 获得群的名称,群内成员昵称等数据;getOpenidToken - 获得用户openid;这些权限微信应该是没有准备开放的。每次在进入小程序时,客户端都需要先去请求该小程序的元数据,例如应用名、版本号、一些权限列表、代码包下载地址等描述信息,修改这些元数据可以获得相应的权限,小程序的关键信息完全由后台控制进行配置,另外小程序的本地文件存储采用HASH映射机制进行文件定位,文件存储在外部存储,本身通过自定义算法实现完整性校验 - 首先,小程序最终存储的文件名是:对称加密(文件流内容Alder32校验和 | 原始文件名)生成的,最终文件名和文件内容会通过自校验判断完整性;其次,本地缓存是通过HASH映射查找文件。所以即使能破解文件名和文件内容,绕过文件自身签名校验,篡改为攻击者的伪造文件,小程序APP也无法映射到该伪造文件进行使用。)

0x02 前端代码安全

由上可见,微信并没有在代码安全上进行过多的考虑。这导致需要在应用审核过程中花费比较多的功夫,不然作品太容易被复制窜改,以至于会失去渠道先机,这对流量是致命打击。由于历史原因,前端的代码安全技术发展的比较缓慢,相比其他被编译成二进制的应用,前端这种纯文本应用,太容易被辨识与窜改。

对前端代码进行保护的目的在于让机器容易识别相关的指令,而使人难以理解代码的逻辑,但往往在对前端代码进行保护过程中,很难既兼顾指令效率又能使可读性降低。因此,常常需要在现有的代码中增加一些额外的验证逻辑,例如一些增加无效的代码进行混淆、采用守护代码保护业务代码不能在其他的域名下正常运行、增加一些防止调试跟踪的断点等,这些措施都是使得破解代码时人工成本增加,从而增加代码的安全性。

下面提供一些能够增加前端代码安全性的策略:

1. 精简(minify)

这是最简单且无害的方法,精简代码能减少代码体积,从而减小数据传输的负荷,同时也能降低代码的可读性。在小程序开发者工具中也提供该选项。对Javascript代码进行精简大致可以从以下几个方面入手:

  • 删除注释,删除无意义或者多余的空白,删除可以省略的符号

  • 删除一些没有调用的代码(Dead code),对函数进行精简(三元运算符?:、字符串操作、对象函数、对象继承、函数引用、无名函数、递归函数)

  • 将变量名进行简化,将零散的变量声明合并,缩短语句

    ......

常用的工具有很多:YUI Compressor、UglifyJS、Google Closure Compiler、JS Packer、JS Min...

使用工具对代码进行精简时需要注意:1. 最好备份原始代码,方便调试与后期修改。 2. 用于调试精简代码时保存的sourcemap,在线上应该删除。 3.编写代码的时候应该严格按照规范,最好使用lint工具对代码进行检查,精简代码后导致代码不可用时,调试非常困难。

这种简单的方法虽然很实用,但是也很容易被还原出源代码,使用一些代码格式化工具可以补齐被删除的空格、换行、符号等,例如jsbeautifier。另外2015年就有相关的研究,从大量的代码中推测出被精简的代码,因为人写代码总有固定的范式,所写的代码相似性都非常的高,如果用统计方式就很容易反推源代码,苏黎世联邦理工大学Martin Vechev教授领带下开发的工具JSNice就是一款运用条件随机场(Conditional Random Fields)机器学习和程序分析方法来还原Javascript代码利器,利用大量的开源代码,去学习命名和类型的规律。JSNice可以用于以下不同的方面:反精简的JavaScript代码、对当前的代码提供更多的更有意义的变量名、自动化程序的注释等。相关论文传送门 后台代码传送门

2. 混淆(obfuscation)

混淆可以减低代码的可读性,防止被轻易追踪出程序逻辑。常见的混淆方法有如下几种:

  • 通过编码混淆代码,这篇文章《Javascript常用混淆方法》里面介绍了很多不错的编码加密方法。但是这些方法有个明显缺点,增加代码体积,而且编码加密都是可逆的。

  • 将标识符混淆和控制逻辑混淆(分离静态资源、打乱控制流、增加无义的代码等),例如aaencode和jjencode。

    标识符混淆的方法有多种,有些与编码混淆代码方法有些重叠,常用方法有哈希函数命名、标识符交换和重载归纳等。哈希函数命名是简单地将原来标识符的字符串替换成该字符串的哈希值,这样标识符的字符串就与软件代码不相关了;标识符交换是指先收集软件代码中所有的标识符字符串,然后再随机地分配给不同的标识符,该方法不易被攻击者察觉;重载归纳是指利用高级编程语言命名规则中的一些特点,例如在不同的命名空间中变量名可以相同,使代码中不同的标识符尽量使用相同的字符串,增加攻击者对软件源代码的理解难度。

    控制混淆是改变程序的执行流程,从而打断逆向分析人员的跟踪思路,达到保护软件的目的。一般采用的技术有插入指令、伪装条件语句、断点等。伪装条件语句是当程序顺序执行从A到B,混淆后在A和B之间加入条件判断,使A执行完后输出TRUE或FALSE,但不论怎么输出,B一定会执行。控制混淆采用比较多的还有模糊谓词、内嵌外联、打破顺序等方法。模糊谓词是利用消息不对称的原理,在加入模糊谓词时其值对混淆者是已知的,而对反混淆者却很难推知。所以加入后将干扰反汇编者对值的分析。模糊谓词的使用一般是插入一些死的或不相关的代码(bogus code),或者是插入在循环或分支语句中,打断程序执行流程。内嵌(in-line)是将一小段程序嵌入到被调用的每一个程序点,外联(out-line)是将没有任何逻辑联系的一段代码抽象成一段可被多次调用的程序。打破顺序是指打破程序的局部相关性。由于程序员往往倾向于把相关代码放在一起,通过打破顺序改变程序空间结构,将加大破解者的思维跳跃[3]。

  • 另外还有些混淆方式是专门针对于反混淆工具设计的,这就需要去仔细分析反混淆工具的原理,在一些特定的地方插入代码使反混淆器进入死循环或者异常跳出。

一般来说,提供代码精简的工具都会提供一些混淆的方法,除此之外,比较知名的商业工具有jasob、jscrambler,一般越商业的越难被反混淆,然而这些高级的代码混淆也常会被用于隐藏应用中的恶意代码。对恶意代码进行混淆是为了躲避杀毒软件的检测,这些代码在被混淆扩充后会难以被识别为恶意软件。相应的也有一些反混淆的工具出现,例如上面提到的JSNice工具能够对混淆的代码进行推理,另外反混淆工具JSDetox专门针对一些混淆方法做过专门的支持。反混淆一直是一项体力活,根据不同的混淆策略需要进行反推演算,这就是一场攻与防的游戏罢了。

3. 加密(encryption)

加密的关键思想在于将需要执行的代码进行一次编码,在执行的时候还原出浏览器可执行的合法的脚本,在某个角度也可以认为是一种混淆的形式,看上去和可执行文件的加壳有点类似。Javascript提供了将字符串当做代码执行(evaluate)的能力,可以通过 Function constructor、eval、setTimeout、setInterval、Worker、DOM event等将字符串传递给JS引擎进行解析执行,由于有些需要用到eval函数,会导致代码性能会减低。以Worker执行举例:

var URL = window.URL || window.webkitURL;var Blob = window.Blob || window.webkitBlob;var blobURL = URL.createObjectURL( new Blob(['console.log("Hello World!")'], {type: 'application/javascript'}));new Worker(blobURL);URL.revokeObjectURL(blobURL);

有以下常见的几种加密方法:

base64编码,一种简单的方法就是将代码转化成base64编码,然后通过atob以及eval进行解码然后运行,另外一种采用base62编码技术更为常见,其最明显的特征是生成的代码以eval(function(p,a,c,k,e,r))开头。无论代码如何进行变形,其最终都要调用一次eval等函数。解密的方法不需要对其算法做任何分析,只需要简单地找到这个最终的调用,改为console.log或者其他方式,将程序解码后的结果按照字符串输出即可。

(PS: 从算法上看,packer是一种base64编码字典压缩策略,packer的base64编码的压缩率很高,精简后代码依然可以减少50%体积以上,因为带有解压器和字符表,越长的代码理论上压缩率更高,想要了解详情可以看看这篇文章《Packer,你对我的JS做了什么!》)

  • 使用复杂化表达式,在Javascript中可以把原本简单的字面量(Literal)、成员访问(MemberExpression)、函数调用(CallExpression)等代码片段变得难以阅读。例如这个方法仅用+!等符号就足以实现几乎任意Javascript代码。在 JS 代码中可以找到许多这样互逆的运算,通过使用随机生成的方式将其组合使用,可以把简单的表达式无限复杂化。

  • 隐写术,将 JS 代码隐藏到了特定的介质当中。如通过最低有效位(LSB)算法嵌入到图片的 RGB 通道、隐藏在图片 EXIF 元数据、隐藏在 HTML 空白字符、放到css文件中(利用content样式能存放字符串的特性)等。比如一张图片黑掉你:在图片中嵌入恶意程序,这个正是使用了最低有效位平面算法,结合HTML5的canvas或者处理二进制数据的TypeArray,抽取出载体中隐藏的数据(如代码)。隐写的方式同样需要解码程序和动态执行,所以破解的方式和前者相同,在浏览器上下文中劫持替换关键函数调用的行为,改为文本输出即可得到载体中隐藏的代码[4]。

  • 混合加密,单个方法容易被破解,但组合起来就不会那么容易了,破解成本也会指数增长,例如jdists采用组合加密和嵌套加密的方式。

这些加密的方式都很容易通过对源代码进行词法分析、语法分析进行还原,首先将代码的字符串转换为抽象语法树(Abstract Syntax Tree, AST)的数据形式,然后从语法树的根节点开始,使用深度优先遍历整棵树的所有节点,根据节点上分析出来的指令逐个执行,直到脚本结束返回结果。这种方法大多数用于对代码进行优化,例如最近Facebook开源了代码优化工具Prepack,可以自动消除冗余代码,降低打包体积和执行时间,基本上就可以用来将这些加密的字符串进行还原,毕竟编码这些字符串都是可以通过词法语法推测出来的。

4. 编译(compile)

Github上有一份清单记录了所有Javascript扩展语言,这些语言都可以通过编译器转化为Javascript语言,这也是前端发展的一个趋势,原来写的html,css,Javascript已经开始变成了一个“中间语言”,而且越来越多的团队也有了自己的一套前端编译系统。Javascript越来越像Web中的汇编语言,特别是近些年Node的普及,让前端变得越来越复杂,大量前端框架的出现,使得Javascript代码可以通过手工编写,也可以从另一种语言编译而来,详情参考几年前Brendan Eich(JavaScript之父)、Douglas Crockford(JSON之父),还有Mike Shaver(Mozilla技术副总裁)的邮件。通过编译后的Javascript代码越方便机器的理解,降低可读性,在某一定角度上讲,这也不愧为一种代码保护措施。据说几大科技巨头正在酝酿给浏览器应用设计一款通用的字节码标准——WebAssembly,一旦这个设想得以实现,代码保护将可以引入真正意义上的“加壳”或者虚拟机保护,对抗技术又将提升到一个新的台阶。目前在桌面端,使用NW.js框架可以JavaScript应用程序的源代码可以被编译为本地代码,在运行时通过NW.js动态还原出源代码,但是这种方法目前会比正常的JS代码慢30%左右。

5. 防止被调试

对代码进行破解分析无非分为静态分析和动态分析,如果对代码进行混淆加密等形式操作,那么静态分析就很麻烦了,对代码调试跟踪分析可以对代码整体逻辑有一个宏观的把控。例如首先判断浏览器是否开启了开发者工具控制台(目前最完美的解决方案传送门),如果检测出控制台开启则堵塞Javascript执行或让代码异常跳出。另外Android 4.4及以上和iOS是支持webkit remote debug的,因此应该在debug模式下,设置代码可以被debug,release模式下,禁止debug以保护数据安全。

6. 前后端协作

首先得强调的事情是不要在前端放敏感数据,前端容易破解,因此需要配合后端进行安全防护,例如微信小程序的登录,必须利用授信的后端配合才能完成此项功能,另外在小程序的网络请求中的referer是不可以设置的,格式固定为https://servicewechat.com/{appid}/{version}/page-frame.html,其中{appid}为小程序的appid,通过验证appid字段可以抵御一些直接的山寨,其次就是加快迭代速度更改代码保护策略,这样可以让之前的分析失效,增加破解的成本。

以上就是对当前前端代码安全进行的探索,最后用一句话结束:

Beneath this mask, there is more than flesh. Beneath this mask, there is an idea. And ideas are bulletproof.

作者:不详

出处:知识商店