关于安全:大型-Web-应用插件化架构探索

30次阅读

共计 5795 个字符,预计需要花费 15 分钟才能阅读完成。

简介:随着 Web 技术的逐步成熟,越来越多的利用架构趋向于简单,例如阿里云等巨型控制台我的项目,每个产品下都有各自的团队来负责保护和迭代。不论是保护还是公布以及管控老本都随着业务体量的增长而逐步不可控。在这个背景下微前端利用而生,微前端在阿里外部曾经有许多成熟的实际,这里不再赘述。本文以微前端为引子(蹭热度),探讨一些另类的 Web 利用所面临的相似问题。

前言

因为篇幅过长,本文会拆分成系列文章,借助 Web 利用的插件架构,介绍

随着 Web 技术的逐步成熟,越来越多的利用架构趋向于简单,例如阿里云等巨型控制台我的项目,每个产品下都有各自的团队来负责保护和迭代。不论是保护还是公布以及管控老本都随着业务体量的增长而逐步不可控。在这个背景下 微前端 利用而生,微前端在阿里外部曾经有许多成熟的实际,这里不再赘述。本文以微前端为引子,探讨一些另类的 Web 利用所面临的相似问题。

古代文本编辑器沉浮

2018 年微软 GitHub 后,Atom便常常被拿来调侃,所谓一山不容二虎。在 VS Code 曾经成为一众前端工程师编辑器首选的当下,Atom 的位置显得很难堪,论性能被同为 Electron 的 VS Code 秒杀,论插件,VS Code 去年插件总数就曾经冲破 1w 大关,而早公布一年多的 Atom 至今还停留在 8k +。再加上微软官网主导的 LSP/DAP 等重量级协定的遍及,时至今日 Atom 作为已经 Web/Electron 技术标杆利用的位置早已被 VS Code 斩落马下。

网上对于 Atom 的日渐衰败的探讨,始终离不开性能。Atom 确实太慢了,究其原因很大水平上是被其插件架构所连累的。尤其是 Atom 在 UI 层面凋谢过多的权限给插件开发者定制,插件品质良萎不齐以及 UI 齐全凋谢给插件后带来的安全隐患都成为 Atom 的阿喀琉斯之踵。甚至其主界面的 FileTree、Tab 栏、Setting Views 等重要组件都是通过插件实现的。相比之下 VS Code 则关闭很多,VS Code 插件齐全运行在 Node.js 端,对于 UI 的定制性只有极个别被封装为纯办法调用的 API。

但另一方面,VS Code 这种绝对关闭的插件 UI 计划,一些须要更强定制性的性能便无奈满足,更多插件开发者开始魔改 VS Code 底层甚至源码来实现定制。例如社区很火的 VS Code Background,这款插件通过强行批改 VS Code 安装文件中的 CSS 来实现编辑器区域的背景图。而另一款 VSC Netease Music 则更激进,因为 VS Code 捆绑包中的 Electron 剔除了 FFmpeg 导致在 Webview 视图下无奈播放音视频,应用此插件须要自行替换 FFmpeg 的动态链接库。而这些插件未免会对 VS Code 安装包造成肯定水平的毁坏,导致用户须要卸载重装。

不止编辑器 – 飞个马

Figma 是一个在线合作式 UI 设计工具,相比 Sketch 它具备跨平台、实时合作等长处,近年来逐步受到 UI 设计师们的青眼。而近期 Figma 也正式上线了其插件零碎。

作为一个 Web 利用,Figma 的插件零碎天然也是基于 JavaScript 构建的,这肯定水平上升高了开发门槛。自去年 6 月份 Figma 官网发表凋谢插件零碎测试以来,曾经有越来越多的 Designner/Developer 开发了 300+ 插件,其中包含图形资源、文件归档、甚至是导入 3D 模型等。

Figma 的插件零碎是如何工作的?

这是一个基于 TypeScript + React 技术栈,应用 Webpack 构建的 Figma 插件目录构造

.
├── README.md
├── figma.d.ts
├── manifest.json
├── package-lock.json
├── package.json
├── src
│   ├── code.ts
│   ├── logo.svg
│   ├── ui.css
│   ├── ui.html
│   └── ui.tsx
├── tsconfig.json
└── webpack.config.js

在其 manifest.json 文件中蕴含了一些简略的信息。

{
  "name": "React Sample",
  "id": "738168449509241862",
  "api": "1.0.0",
  "main": "dist/code.js",
  "ui": "dist/ui.html"
}

能够看出 Figma 将插件入口分为了 mainui 两局部,main 中蕴含了插件理论运行时的逻辑,而 ui 则是一个插件的 HTML 片段。即 UI 与逻辑拆散。装置一个 Color Search 插件后察看页面构造能够发现 main 中的 js 文件被包裹在一个 iframe 里加载到页面上,对于 main 入口的沙箱机制后文中有具体的论述。而 ui 中的 HTML 最终也被包裹在一个 iframe 里渲染进去,这将无效的防止插件 UI 层 CSS 代码导致全局款式净化。

Figma Developers 文档中 有一章节 How Plugins Run 对其插件零碎运行机制进行了简略的介绍,简略来说 Figma 为插件中逻辑层的 main 入口创立了一个最小的 JavaScript 执行环境,它运行在浏览器主线程上,在这个执行环境中插件代码无法访问到一些浏览器全局的 API,从而也就无奈在代码层面对 Figma 自身运行造成影响。而 UI 层有且仅有一份 HTML 代码片段,在插件被激活后被渲染到一个弹窗中。

Figma 官网博客中对其插件的沙箱机制做了具体的论述。起初他们尝试的计划是 iframe,一个浏览器自带的沙箱环境。将插件代码由 iframe 包裹起来,因为 iframe 人造的限度,这将确保插件代码无奈操作 Figma 主界面上下文,同时也能够只凋谢一份白名单 API 供插件调用。乍一看仿佛解决了问题,但因为 iframe 中的插件脚本只能通过 postMessage 与主线程通信,这导致插件中的任何 API 调用都必须被包装为一个异步 async/await 的办法,这无疑对 Figma 的指标用户 非专业前端开发者的设计师 不够敌对。其次对于较大的文档,postMessage 通信序列化的性能老本过高,甚至会导致内存透露。

Figma 团队抉择回到浏览器主线程,但间接将第三方代码运行在主线程,由此引发的平安问题是不可避免的。最终他们发现了一个尚在 stage2 阶段的草案 Realm API。Realm 旨在创立一个畛域对象,用于隔离第三方 JavaScript 作用域的 API。

let g = window; // outer global
let r = new Realm(); // root realm

let f = r.evaluate("(function() {return 17})");

f() === 17 // true

Reflect.getPrototypeOf(f) === g.Function.prototype // false
Reflect.getPrototypeOf(f) === r.globalThis.Function.prototype // true

值得注意的是,Realm 同样能够应用 JavaScript 目前已有的个性来实现,即 withProxy。这也是目前社区比拟风行的沙箱计划。

const whitelist = {
  windiw: undefined,
  document: undefined,
  console: window.console,
};

const scopeProxy = new Proxy(whitelist, {get(target, prop) {if (prop in target) {return target[prop]
    }
    return undefined
  }
});

with (scopeProxy) {eval("console.log(document.write)") // Cannot read property 'write' of undefined!
  eval("console.log('hello')")        // hello
}

前文中 Figma 插件被 iframe 所包裹的插件 main 入口即蕴含了一个被 Realm 接管的作用域,你能够认为是相似这段示例代码中的一份 白名单 API,毕竟保护一份白名单比屏蔽黑名单实现起来更简洁。但事实上因为 JavaScript 的原型式继承,插件依然能够通过 console.log 办法的原型链拜访到内部对象,现实的解决方案是将这些白名单 API 在 Realm 上下文中包装一次,从而彻底隔离原型链。

const safeLogFactory = realm.evaluate(`
  (function safeLogFactory(unsafeLog) {return function safeLog(...args) {unsafeLog(...args);
    }
  })
`);

const safeLog = safeLogFactory(console.log);

const outerIntrinsics = safeLog instanceOf Function;
const innerIntrinsics = realm.evaluate(`log instanceOf Function`, { log: safeLog});
if (outerIntrinsics || !innerIntrinsics) throw new TypeError(); 

realm.evaluate(`log("Hello outside world!")`, {log: safeLog});

显然为每一个白名单中的 API 做这样操作的工作是十分繁冗且容易出错的。那么如何构建一个平安且易于增加 API 的沙箱环境呢?

Duktape 是一个由 C++ 实现的用于嵌入式设施的 JavaScript 解释器,它不反对任何浏览器 API,天然地它能够被编译到 WebAssembly,Figma 团队将 Duktape 嵌入到 Realm 上下文中,插件最终通过 Duktape 解释执行。这样能够平安的实现插件所需 API,且不必放心插件会通过原型链拜访到沙箱内部。

这是一种被称为 Membrane Pattern 的防御性的编程模式,用于在程序中与子组件 (狭义上) 实现一层中介。简略来说就是代理(Proxy),为一个对象创立一个可控的拜访边界,使得它能够保留一部分个性给第三方嵌入脚本,而屏蔽一部分不心愿被拜访到的个性。对于 Membrane 的具体阐述能够查看 Isolating application sub-components with membranes 与 Membranes in JavaScript 这两篇文章。

这是最终 Figma 的插件计划,它运行在主线程,不须要放心 postMessage 通信带来的传输损耗。多了一次 Duktape 解释执行的耗费,但得益于 WebAssembly 杰出的性能,这部分耗费并不是很大。

另外 Figma 还保留了最后的 iframe,容许插件能够自行创立 iframe,并在其中插入任意 JavaScript,同时它能够与沙箱中的 JavaScript 脚本通过 postMessage 互相通信。

鱼和熊掌如何兼得?

咱们把这类插件的需要总结为 在 Web 利用中运行第三方代码及其自定义控件 ,它有与结尾提到的 微前端 架构十分类似的一些问题。

  1. 肯定水平上的 JavaScript 代码沙箱隔离机制,利用主体对第三方代码 (或子利用) 有肯定的管控能力
  2. 款式强隔离,第三方代码款式不对利用主体产生 CSS 净化

JavaScript 沙箱

JavaScript 沙箱隔离在社区是个经久不衰的话题,最简略的 iframe 标签 Sandbox 属性就曾经能做到 JavaScript 运行时的隔离,社区较为风行的是利用一些语言个性 (with、realm、Proxy 等 API) 屏蔽(或代理) Window、Document 等全局对象,建设白名单机制,对可能潜在危险操作的 API 重写(如阿里云 Console OS – Browser VM)。另外还有 Figma 这种尝试嵌入平台无关的 JavaScript 解释器,所有第三方代码都通过嵌入的解释器来执行。以及利用 Web Worker 做 DOM Diff 计算,并将计算结果发送回 UI 线程来进行渲染,这个计划早在 2013 年就曾经有人进行了实际,这篇论文中作者将 JSDOM 这一 Node.js 平台宽泛风行的测试库运行在 Web Worker。而近些年来也有 preact-worker-demo、react-worker-dom 等我的项目基于 Web Worker 的 DOM Renderer 尝试将 DOM API 代理到 Worker 线程。而 Google AMP Project 在 JSCONF 2018 US 对外颁布的 worker-dom 则将 DOM API 在 Web Worker 端实现了 DOM API,尽管实际下来还存在一些问题(例如同步办法无奈模仿),但 WorkerDOM 在性能和隔离性上都获得了肯定成绩。

以上这些解决方案被宽泛的利用在各种插件化架构的 Web 利用中,但大多都是 Case By Case,每种解决方案都有各自的老本与取舍。

CSS 作用域

CSS 款式隔离计划中,如上文中 Figma 应用 iframe 渲染插件界面,就义一部分性能换来了绝对完满的款式隔离。而在古代前端工程化体系下,能够通过 CSS Module 在转译时对 class 增加 hash 或 namespace 等形式实现,这类计划较为依赖插件代码编译过程。而更新潮的是利用 Web Component 的 Shadow DOM,将插件元素用 Web Component 包裹起来,Shadow Root 内部款式无奈作用于外部,同样 Shadow Root 外部的款式也无奈影响到内部。

最初

本文列举了目前编辑器、设计工具这类大型 Web 利用插件化架构下所面临的的一些问题,以及社区实际的解决方案。不论是让人又爱又恨的 iframe,还是 Realm、Web Worker、Shadow DOM 等,目前来说每种计划都有各自的劣势与有余。但随着 Web 利用的复杂度增长,插件化这一需要也逐步被各大标准化组织所器重起来。下一篇将着重介绍 KAITIAN IDE 中插件架构的摸索与实际,包含 JavaScript 沙箱、CSS 隔离、Web Worker 等。

版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

正文完
 0