人人都会AI编程

预加载脚本(preload):安全桥梁、能力受控暴露

更新时间:2026-07-11

如果你直接从渲染进程调用 Node.js 或 Electron 的原生 API,就像给网页开了后门——任何一个 XSS 漏洞都可能让攻击者读取用户硬盘上的任意文件。预加载脚本的存在,就是为了在这条危险的捷径上建一道安检关卡:它运行在一个介于纯网页环境和完整 Node.js 环境之间的特权沙箱中,可以有选择地向渲染进程暴露有限且安全的能力。

为什么需要预加载脚本

在早期的 Electron 版本中,渲染进程可以直接开启 nodeIntegration: true,这意味着你的 index.html 里的 <script> 标签内就能 require('fs') 并读写本地文件。这样做开发起来确实方便,但也极其危险:

  • 任何第三方脚本或被注入的恶意代码都能直接控制用户的文件系统;
  • 你无法控制哪些 API 会被调用、以什么参数调用;
  • 一旦出现安全漏洞,攻击面是整个 Node.js 运行时。

现代 Electron(v12 起默认启用 contextIsolation: true 并禁用 nodeIntegration)的做法是把这道门完全关上,然后只开一扇小窗——这扇窗就是预加载脚本。

预加载脚本的工作方式

预加载脚本是一个独立的 JavaScript 文件,在网页内容加载之前运行。它的运行环境很特殊:

  • 它拥有完整的 Node.js 和 Electron API 访问权限(可以 require('fs')require('electron'));
  • 但它与网页内容运行在不同的 JavaScript 上下文中,两者并不直接共享全局变量;
  • 它可以使用 contextBridge 将特定的功能安全地暴露给网页。

具体流程是:

  1. 主进程创建窗口时,通过 webPreferences.preload 指定预加载脚本的路径。
  2. 窗口加载网页内容之前,Electron 先执行预加载脚本。
  3. 预加载脚本使用 contextBridge.exposeInMainWorld 向渲染进程的全局作用域注入一个自定义 API 对象。
  4. 网页端通过这个注入的对象调用被暴露的方法,预加载脚本内部再通过 ipcRenderer 向主进程转发请求。
  5. 主进程接收到请求后,在权限受控的环境中完成实际操作(如读文件、显示系统对话框),并将结果返回。

这样一来,网页完全接触不到 fschild_process 这些危险模块,只能调用你预先定义好的几个函数。攻击者即使完全控制了渲染进程,也只能得到你主动暴露的那几个方法,而无法为所欲为。

能力受控暴露:只给需要的,不给全部的

“受控暴露”的核心思想是最小权限原则:只让渲染进程调用那些它完成业务逻辑所必须的方法,而且对参数和返回结果进行严格的验证与过滤。

一个典型的预加载脚本长这样:

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // 暴露一个安全的“打开文件”功能
  openFile: () => ipcRenderer.invoke('dialog:openFile'),

  // 暴露一个“保存文件”功能,但调用方不能决定保存路径
  saveContent: (content) => ipcRenderer.invoke('file:save', content),

  // 暴露一个只读的系统信息获取方法
  getAppVersion: () => ipcRenderer.invoke('app:getVersion'),

  // 单向事件监听:主进程有通知时触发回调
  onNotification: (callback) => {
    ipcRenderer.on('notification', (event, message) => callback(message));
  }
});

在渲染进程(你的 Vue/React 组件)中,你就可以通过 window.electronAPI.openFile() 来发起文件选择,完全不需要知道底层是 dialog.showOpenDialog,更接触不到 fs 模块。

如果你想进一步限制,还可以在预加载脚本里对参数做校验:

contextBridge.exposeInMainWorld('safeAPI', {
  deleteFile: (filePath) => {
    // 禁止删除系统关键目录
    if (typeof filePath !== 'string' || filePath.includes('/System/')) {
      throw new Error('不允许的操作');
    }
    return ipcRenderer.invoke('file:delete', filePath);
  }
});

这样即便渲染进程代码被人篡改,也无法随意删除文件。

真实开发中的最佳实践

  1. 永远不要重新启用 nodeIntegration。除非你在开发一个完全不联网、完全可信的内部工具,否则出于安全考虑,也应保持默认的 contextIsolation: truenodeIntegration: false
  1. 预加载脚本只做桥接,不处理业务逻辑。它应该很薄,主要工作就是参数校验和 IPC 封装。真正的业务逻辑放在主进程或渲染进程自己的层里。
  1. 避免直接将 Electron 的复杂对象暴露出去。比如不要把整个 ipcRenderer 对象或 process 对象直接挂在 window 上,否则等于没做限制。只暴露用得到的、精确命名的函数。
  1. 对从主进程传回的数据保持警惕。哪怕数据源头是你自己的主进程,也应将它视为可能被篡改的数据(如果主进程代码存在漏洞)。在渲染进程侧对返回值做基本的类型和格式检查是个好习惯。
  1. 在开发与生产中使用同一策略。不要在开发时为了方便而打开 nodeIntegration,等到打包前再关掉——这样你会发现很多功能在关闭后没法工作,而且安全习惯也培养不起来。

通过预加载脚本实现的能力桥梁,你既保留了 Electron 赋予桌面应用的强大功能,又把安全风险锁定在可控的范围内。它是现代 Electron 应用架构中不可缺失的一环,也是从“能跑起来”到“能安全上生产”的关键一步。