• 微前端是如何实现作用域隔离的?


    微前端是如何实现作用域隔离的?


    111

    一、前言

    沙箱(Sandbox)是一种安全机制,目的是让程序运行在一个相对独立的隔离环境,使其不对外界的程序造成影响,保障系统的安全。作为开发人员,我们经常会同沙箱环境打交道,例如,服务器中使用 Docker 创建应用容器;使用 Codesandbox运行 Demo示例;在程序中创建沙箱执行动态脚本等。

    二、使用场景

    2.1 iPaaS 可视化 API 编排  

    在流程编排的某些节点需要用到低代码模型转换(Transformer),用户可在转换器流程节点自定义 Groovy 脚本实现,服务端在执行自定义的 Groovy 脚本时,会放置在沙箱中,避免对整个流程逻辑造成影响。

    2.2 微前端应用沙箱  

    在微前端当中,有一些全局对象在所有的应用中需要共享,如 Window 对象。不同开发团队的子应用很难通过规范约束他们使用全局变量。为了保证应用的可靠性,需要技术手段去治理运行时的冲突问题;通过使用沙箱,每个前端应用都可以拥有自己的上下文环境、页面路由和状态管理,而不会相互干扰或冲突。

    接下来的篇章我们将介绍大前端领域沙箱的实现以及我们如何基于JS沙箱落地应用的过程。

    三、JS沙箱调研

    3.1 eval和Function  

    前端常见的动态执行代码的方式是使用 Eval 和 New Function 提供一个运行外部代码的环境:     

    1. // 使用 eval 的糟糕代码:
    2. function looseJsonParse(obj){
    3. return eval(`(${obj})`);
    4. }
    5. console.log(looseJsonParse(
    6. "{a:(4-1), b:function(){}, c:new Date()}"
    7. ))
    8. // 使用 Function 的更好的代码:
    9. function looseJsonParse(obj){
    10. return Function(`"use strict";return (${obj})`)();
    11. }
    12. console.log(looseJsonParse(
    13. "{a:(4-1), b:function(){}, c:new Date()}"
    14. ))

    两种方式都可以正常执行,并且返回结果相同,但是用来创建沙箱环境还不够格,因为它们都能访问[全局变量],无法实现作用域隔离。

    3.2 with + new Function + proxy实现  

    3.2.1 with关键字  

    JavaScript 在查找某个未使用命名空间的变量时,会通过作用于链来查找,而 with 关键字,可以使得查找时,先从该对象的属性开始查找,若该对象没有要查找的属性,顺着上一级作用域链查找,若不存在要查到的属性,则会返回 ReferenceError 异常。

    不推荐使用 with,在 ECMAScript 5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。

    3.2.2 ES6 Proxy  

    Proxy 是 ES6 提供的新语法,Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。示例如下:

    1. const handler = {
    2. get: function (obj, prop) {
    3. return prop in obj ? obj[prop] : 'weimob';
    4. },
    5. };
    6. const p = new Proxy({}, handler);
    7. p.a = 2023;
    8. p.b = undefined;
    9. console.log(p.a, p.b); // 2023 undefined
    10. console.log('c' in p, p.c); // false, weimob    

    3.2.3 Symbol.unScopables  

    With 再加上 Proxy 几乎完美解决 JS 沙箱机制。但是如果对象的Symbol.unScopables设置为 true ,会无视 with 的作用域直接向上查找,造成沙箱逃逸,所以要另外处理 Symbol.unScopables。

    3.2.4 沙箱实现   

    1. function sandbox(code, context) {
    2. context = context || Object.create(null);
    3. const fn = new Function('context', `with(context){return (${code})}`);
    4. const proxy = new Proxy(context, {
    5. has(target, key) {
    6. if (["console", "setTimeout", "Date"].includes(key)) {
    7. return true
    8. }
    9. if (!target.hasOwnProperty(key)) {
    10. throw new Error(`Illegal operation for key ${key}`)
    11. }
    12. return target[key]
    13. },
    14. get(target, key, receiver) {
    15. if (key === Symbol.unscopables) {
    16. return undefined;
    17. }
    18. return Reflect.get(target, key, receiver);
    19. }
    20. })
    21. return fn.call(proxy, proxy);
    22. }
    23. sandbox('3+2') // 5
    24. sandbox('console.log("智慧商业服务商")') // Cannot read property 'log' of undefined
    25. sandbox('console.log("智慧商业服务商")', {console: window.console}) // 智慧商业服务商
    26.        

    上面的代码主要做了3件事,实现沙箱隔离:

    • 使用 with API,将对象添加到作用域链的顶部,变量访问会优先查找你传入的参数对象,之后再往上找;
    • 通过ES6提供的proxy,设置has函数,实现对象的访问拦截,同时处理Symbol.unscopables 的属性,控制可以被访问的变量 context,阻断沙箱内的对外访问;
    • 绑定 this 指向 proxy 对象,防止 this 访问 window;

    3.3 基于iframe实现  

    iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离。在 iframe 中运行的脚本程序访问到的全局对象均是当前 iframe 执行上下文提供的,不会影响其父页面的主体功能,因此使用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法。

    1. const parent = window;
    2. const frame = document.createElement('iframe');
    3. // 限制代码 iframe 代码执行能力
    4. frame.sandbox = 'allow-same-origin';
    5. document.body.appendChild(iframe);
    6. const sandboxGlobal = iframe.contentWindow;

    3.4 node运行时实现  

    3.4.1 原生模块vm  

    相较于浏览器环境,Node运行时就简单很多,使用其提供的原生vm模块,可以很方便的创建V8虚拟机,并在指定上下文编译和执行代码;

    1. const vm = require('node:vm');
    2. const x = 1;
    3. const context = { x: 2 };
    4. vm.createContext(context); // Contextify the object.
    5. const code = 'x += 40; var y = 17;';
    6. vm.runInContext(code, context);
    7. console.log(context.x); // 42
    8. console.log(context.y); // 17
    9. console.log(x); // 1; y is not defined.

    问题来了,使用 vm.runInContext 看似创建了沙箱隔离环境,但 vm 模块足够安全吗?引用 Node 官网的回答

    node:vm 模块不是安全机制。不要用它来运行不受信任的代码。

    3.4.2 不安全原因  

    为什么不是安全机制,继续剖析;

    1. const vm = require('vm');
    2. vm.runInNewContext('this.constructor.constructor("return process")().exit()');
    3. console.log('智慧商业服务商') // 永远不会执行

    这就是 JS 语言的特性,以上示例中 runInNewContext 会默认创建上下文对象, this 指向默认创建的 ctx 对象 并通过原型链的方式拿到沙盒外的 Funtion,通过Function 访问全局变量,完成逃逸,并执行逃逸后的 JS 代码。

    3.4.3 解决方案  

    解决方案是绑定上下文对象,同时切断上下文对象的原型链,提供纯净的上下文对象,避免通过原型链逃逸。

    1. const vm = require('vm');
    2. let sandBox = Object.create(null);
    3. sandBox.title = '智慧商业服务商'
    4. sandBox.console = console
    5. vm.runInNewContext('console.log(title)', sandBox);
  • 相关阅读:
    C#泛型委托
    力扣刷题-二叉树-二叉树的高度与深度
    Unity关键词语音识别
    源码中的设计模式--单例模式
    SpringBoot+uniapp+uview打造H5+小程序+APP入门学习的聊天小项目
    Hadoop简介
    DDIM代码详细解读(3):核心采样代码、超分辨率重建
    使用 乐天 / V-IM 作为网页即时聊天
    nvme cmd
    python设计模式11:观察者模式
  • 原文地址:https://blog.csdn.net/m0_69824302/article/details/138174253