Preload 桥接与安全模型.md 23 KB

Preload 桥接与安全模型

本文引用的文件

  • bridge.ts
  • index.ts
  • lifecycle.ts
  • main.ts
  • windowOptions.ts
  • config.default.ts
  • osCtl.js
  • ipcRenderer.ts
  • useElectronWindow.ts

目录

  1. 引言
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细组件分析
  6. 依赖关系分析
  7. 性能与安全性考虑
  8. 故障排查指南
  9. 结论
  10. 附录:新增安全 IPC 接口步骤

引言

本文聚焦 Electron 渲染进程的安全访问模型,重点说明在启用 contextIsolation 后,为什么不能直接在渲染进程中使用 Electron API,以及当前仓库中 preload、bridge、IPC 通道和前端调用链路如何协作。同时给出最小权限暴露策略、跨进程通信边界、常见错误原因,以及新增一个安全 IPC 接口的完整示例路径。

项目结构

与 preload 桥和安全模型直接相关的代码主要分布在以下位置:

  • 主进程入口与生命周期注册:ai-electron/electron/main.ts、ai-electron/electron/preload/lifecycle.ts
  • Preload 初始化逻辑:ai-electron/electron/preload/index.ts
  • 上下文桥接暴露:ai-electron/electron/preload/bridge.ts
  • 窗口与 WebPreferences 配置:ai-electron/electron/config/windowOptions.ts、ai-electron/electron/config/config.default.ts
  • 主进程控制器(IPC 处理):ai-electron/electron/controller/osCtl.js
  • 前端 IPC 封装与 Hook:ai-electron/frontend/src/core/utils/desktop/ipcRenderer.ts、ai-electron/frontend/src/core/hooks/desktop/useElectronWindow.ts

    graph TB
    Main["主进程 main.ts"] --> Lifecycle["生命周期 lifecycle.ts"]
    Main --> Preload["预加载 index.ts"]
    Preload --> TrayService["托盘服务 trayService"]
    Preload --> SecurityService["安全服务 securityService"]
    Bridge["上下文桥 bridge.ts"] --> Window["渲染进程 window.electron"]
    Config["窗口配置 windowOptions.ts<br/>默认配置 config.default.ts"] --> Window
    FrontendIpc["前端 IPC 封装 ipcRenderer.ts"] --> Window
    FrontendHook["窗口 Hook useElectronWindow.ts"] --> FrontendIpc
    FrontendIpc --> IpcChannel["IPC 通道 controller/osCtl/*"]
    IpcChannel --> OsCtl["主进程控制器 osCtl.js"]
    

图表来源

  • main.ts:1-19
  • lifecycle.ts:1-73
  • index.ts:1-16
  • bridge.ts:1-10
  • windowOptions.ts:1-41
  • config.default.ts:1-50
  • osCtl.js:1-169
  • ipcRenderer.ts:1-51
  • useElectronWindow.ts:1-87

章节来源

  • main.ts:1-19
  • index.ts:1-16
  • bridge.ts:1-10
  • windowOptions.ts:1-41
  • config.default.ts:1-50

核心组件

本节从安全角度解释每个关键文件的作用。

上下文桥 bridge.ts

该文件通过 contextBridge.exposeInMainWorld('electron', {...}) 向渲染进程暴露受控对象。当前仅暴露 ipcRenderer,这是实现“只让渲染进程使用经过主进程控制的 IPC 能力”的最小集合。它体现了最小权限原则:不暴露 Node.js、文件系统、子进程、窗口管理等危险 API。

章节来源

  • bridge.ts:1-10

预加载入口 preload/index.ts

preload() 函数在渲染进程上下文之前运行,负责创建托盘服务和安全服务。其设计目标是把需要长期存在的主进程服务启动逻辑放在 preload 阶段执行,而不是在渲染进程中直接引入 Electron 或 Node 能力。这样可以避免渲染进程直接持有系统级能力,降低被恶意脚本利用的风险。

当前实现还包含日志输出和占位注释,表明后续可扩展更多服务初始化逻辑。

章节来源

  • index.ts:1-16

主进程生命周期 lifecycle.ts

Lifecycle 类注册应用就绪、Electron 应用就绪、窗口就绪、关闭前等生命周期钩子。虽然它本身不是 preload 桥的一部分,但它展示了主进程对窗口、屏幕、焦点、显示时机等系统能力的集中管理。这些能力应始终由主进程控制器封装后通过 IPC 暴露,而不能由渲染进程直接操作。

章节来源

  • lifecycle.ts:1-73

主进程入口 main.ts

main.ts 创建应用实例、注册生命周期钩子,并把 preload 函数注册到应用生命周期中。这说明 preload 是主进程生命周期的一部分,但它在渲染页面加载前执行,从而可以在安全边界内初始化服务并暴露最小化接口。

章节来源

  • main.ts:1-19

窗口配置 windowOptions.ts 与 config.default.ts

当前默认配置将 contextIsolation 设为 false,并将 nodeIntegration 设为 true。配置文件注释明确说明:渲染进程目前依赖 nodeIntegration 直接 require('electron') 调用 IPC;如果智能工作台窗口单独启用 contextIsolation,而 preload 没有相应启用,两个窗口行为会不一致。

这意味着当前代码库的实际安全模型与“理想安全模型”之间存在差异:

  • 理想模型:启用 contextIsolation,禁用 nodeIntegration,仅通过 preload 暴露最小 API。
  • 当前模型:未启用 contextIsolation,渲染进程可直接使用 Electron API,bridge 的存在更像兼容层而非唯一安全边界。

因此,bridge 暴露的接口必须继续保持最小权限,即使当前渲染进程可能拥有更大能力。

章节来源

  • windowOptions.ts:1-41
  • config.default.ts:1-50

主进程控制器 osCtl.js

OsCtl 是 IPC 控制器的代表,封装了窗口控制、文件读取、目录选择、外部链接打开、应用信息获取、流式上传等功能。所有敏感能力都集中在主进程侧,并通过统一的返回值信封 { success, data | message } 返回给前端。这样前端不需要直接操作 fs、path、shell 等危险模块。

其中 resolveSenderWindow 根据 IPC 事件发送方解析目标窗口,使无边框窗口的最小化、最大化、关闭等操作能正确作用于发起者或主窗口。

章节来源

  • osCtl.js:1-169

前端 IPC 封装 ipcRenderer.ts

前端封装提供两个关键能力:

  1. resolveIpcRenderer():优先从 window.electron.ipcRenderer 获取,回退到 require('electron').ipcRenderer,浏览器环境返回 null。
  2. invokeIpc():统一构造 controller/<ctl>/<method> 通道,调用主进程控制器方法。
  3. onIpc():订阅主进程推送,返回取消订阅函数。

该封装兼容当前两种运行方式:既支持未来启用 preload + bridge 后的 window.electron,也兼容当前 nodeIntegration: true 的 require 方式。

章节来源

  • ipcRenderer.ts:1-51

前端窗口 Hook useElectronWindow.ts

该 Hook 封装无边框窗口的常用操作:最小化、最大化、切换最大化、关闭窗口,并在挂载时同步窗口状态。它同样使用 resolveIpcRenderer() 获取 IPC 能力,并把方法名拼接到 controller/osCtl/ 通道下。

章节来源

  • useElectronWindow.ts:1-87

架构总览

下图展示从前端 UI 到主进程控制器的完整 IPC 流程,以及 preload 桥在其中的作用。

sequenceDiagram
    participant UI as "前端界面"
    participant Hook as "useElectronWindow.ts"
    participant Utils as "ipcRenderer.ts"
    participant Window as "渲染进程 window.electron"
    participant Bridge as "preload bridge.ts"
    participant Controller as "主进程 osCtl.js"
    
    UI->>Hook: 触发窗口操作
    Hook->>Utils: invokeIpc(method, params)
    Utils->>Window: resolveIpcRenderer()
    Window-->>Utils: ipcRenderer.invoke(channel, params)
    Utils->>Controller: IPC 通道 controller/osCtl/method
    Controller->>Controller: resolveSenderWindow(event)
    Controller-->>Utils: { success, data | message }
    Utils-->>Hook: Promise 结果
    Hook-->>UI: 更新状态或提示

图表来源

  • useElectronWindow.ts:1-87
  • ipcRenderer.ts:1-51
  • bridge.ts:1-10
  • osCtl.js:1-169

详细组件分析

安全边界与最小权限暴露策略

当前仓库的安全边界体现在三个层面:

  1. API 暴露面最小化
    bridge.ts 只暴露 ipcRenderer,不暴露 BrowserWindow、fs、child_process、shell、screen 等危险能力。即使未来启用 contextIsolation,这个暴露面也应保持不变。

  2. 能力集中到主进程控制器
    所有涉及窗口、文件、系统路径、外部链接、应用信息的操作都进入 osCtl.js,由主进程统一校验、处理、记录日志,并以结构化信封返回。

  3. 前端通过统一封装调用
    前端不应直接拼接原始 IPC 通道字符串去猜测主进程 API,而是通过 ipcRenderer.ts 提供的 invokeIpc 和 onIpc,保持通道命名规范 controller/<ctl>/<method>。

    flowchart TD
    Start["前端请求"] --> CheckBridge["检查 window.electron.ipcRenderer"]
    CheckBridge -->|存在| UseBridge["使用 preload 暴露的 IPC"]
    CheckBridge -->|不存在| FallbackRequire["回退到 require('electron')"]
    FallbackRequire --> RequireOk{"require 成功?"}
    RequireOk -->|是| UseRequire["使用 nodeIntegration 的 IPC"]
    RequireOk -->|否| NotAvailable["当前环境不可用"]
    UseBridge --> Channel["构造 controller/<ctl>/<method]"]
    UseRequire --> Channel
    Channel --> Controller["主进程控制器校验和处理"]
    Controller --> Result["返回 { success, data | message }"]
    NotAvailable --> Error["抛出桌面端不可用错误"]
    

图表来源

  • ipcRenderer.ts:1-51
  • osCtl.js:1-169

章节来源

  • bridge.ts:1-10
  • ipcRenderer.ts:1-51
  • osCtl.js:1-169

为什么不应把 BrowserWindow、fs、child_process 等暴露到 window.electron

  • BrowserWindow:允许任意创建、销毁、操控窗口,可绕过应用自身的窗口管理策略。
  • fs:允许读写任意本地文件,可能泄露用户数据或写入恶意文件。
  • child_process:允许执行任意系统命令,是最危险的 Node.js 能力之一。
  • shell:可打开外部程序、复制粘贴、访问系统剪贴板。
  • screen:可探测显示器布局,配合其他能力可用于指纹采集或攻击辅助。

当前 bridge.ts 仅暴露 ipcRenderer,符合最小权限原则。即使当前 windowOptions.ts 中 contextIsolation 为 false,也不意味着应该扩大 bridge 暴露面。

章节来源

  • bridge.ts:1-10
  • windowOptions.ts:1-41

预加载职责与运行时机

preload() 在渲染进程上下文之前运行,适合用于:

  • 初始化托盘服务
  • 初始化安全服务
  • 设置全局安全策略
  • 暴露最小化的 window.electron 接口

它不适合做以下事情:

  • 直接大量初始化业务数据
  • 暴露复杂业务逻辑
  • 替代主进程控制器处理敏感系统调用

章节来源

  • index.ts:1-16
  • main.ts:1-19

依赖关系分析

graph LR
    Bridge["bridge.ts"] --> ElectronAPI["Electron contextBridge / ipcRenderer"]
    IndexTs["preload/index.ts"] --> TrayService["trayService.create()"]
    IndexTs --> SecurityService["securityService.create()"]
    MainTs["main.ts"] --> IndexTs
    MainTs --> LifecycleTs["lifecycle.ts"]
    WindowOptions["windowOptions.ts"] --> ConfigDefault["config.default.ts"]
    FrontendIpc["frontend ipcRenderer.ts"] --> WindowElectron["window.electron"]
    FrontendHook["useElectronWindow.ts"] --> FrontendIpc
    FrontendIpc --> OsmCtl["osCtl.js"]

图表来源

  • bridge.ts:1-10
  • index.ts:1-16
  • main.ts:1-19
  • windowOptions.ts:1-41
  • config.default.ts:1-50
  • ipcRenderer.ts:1-51
  • useElectronWindow.ts:1-87
  • osCtl.js:1-169

章节来源

  • main.ts:1-19
  • index.ts:1-16
  • windowOptions.ts:1-41
  • config.default.ts:1-50
  • ipcRenderer.ts:1-51
  • osCtl.js:1-169

性能与安全性考虑

  • 性能

    • preload 中只做必要初始化,避免阻塞渲染进程启动。
    • 大文件或流式数据应通过主进程控制器处理,避免一次性把大数据放入 IPC 消息体。
    • 窗口状态同步应避免高频重复刷新;当前 useElectronWindow.ts 使用防抖思路监听 resize,减少频繁查询。
  • 安全性

    • 坚持最小权限暴露:bridge 只暴露 IPC,不暴露 Node/Electron 内部对象。
    • 所有系统能力集中在主进程控制器,统一校验参数、记录日志、返回结构化结果。
    • 前端不应信任来自主进程的 payload,仍应做基础类型校验。
    • 在启用 contextIsolation 前,要确保所有窗口共享一致的 webPreferences,避免行为漂移。

[本节为通用指导,不直接分析具体文件]

故障排查指南

contextBridge 报错

现象
启动时报错,提示 contextBridge 或 exposeInMainWorld 不可用。

可能原因

  • 使用了不支持 contextBridge 的环境。
  • preload 脚本没有被 Electron 正确加载。
  • 当前实际运行环境仍依赖 nodeIntegration,bridge 只是兼容层。

处理建议

  • 检查是否通过 Electron 的 preload 机制加载该脚本。
  • 检查窗口配置中的 preload 路径是否指向编译后的 bridge 文件。
  • 在当前仓库中,由于 contextIsolation: false,前端更可能走 require('electron') 路径;若确实要启用 contextIsolation,需同时调整 windowOptions.ts 和 config.default.ts,并确保所有窗口行为一致。

章节来源

  • bridge.ts:1-10
  • windowOptions.ts:1-41
  • config.default.ts:1-50

window.electron 为 undefined

现象
前端调用 window.electron.ipcRenderer 时报 undefined。

可能原因

  • preload 未执行或执行失败。
  • bridge 未正确暴露 electron。
  • 当前运行环境未使用 preload,而是依赖 nodeIntegration。
  • 在浏览器环境中运行,既没有 preload,也没有 Electron。

处理建议

  • 确认前端使用 resolveIpcRenderer() 获取 IPC 能力,它能同时兼容 window.electron 和 require('electron')。
  • 确认非 Electron 环境下调用 invokeIpc 会抛出“当前环境不可用”的错误,而不是静默失败。
  • 如果是浏览器调试,不要期望 window.electron 存在。

章节来源

  • ipcRenderer.ts:1-51
  • useElectronWindow.ts:1-87

IPC 事件无响应

现象
前端调用了 IPC,但主进程没有回调,或返回错误。

可能原因

  • 通道名称不匹配。当前约定为 controller/<ctl>/<method>。
  • 主进程控制器未注册该方法。
  • 主进程方法抛异常,但未返回结构化结果。
  • 发送方窗口解析失败,导致操作了错误窗口。

处理建议

  • 核对前端调用方法名是否与 osCtl.js 中方法一致。
  • 检查主进程日志,确认 IPC 到达控制器。
  • 检查 resolveSenderWindow 是否能拿到有效窗口。
  • 对可能失败的方法,检查返回的 { success, message }。

章节来源

  • ipcRenderer.ts:1-51
  • osCtl.js:1-169

权限不足

现象
某些操作在前端可用,但主进程拒绝或返回错误。

可能原因

  • 主进程控制器对参数做了校验,例如目录必须是绝对路径、必须存在且是目录。
  • 前端传入了非法路径或空值。
  • 当前环境不是桌面客户端,无法调用系统能力。

处理建议

  • 查看主进程控制器返回的 message。
  • 前端在调用前尽量做基础校验,但仍以主进程为准。
  • 在非桌面环境使用 isDesktopDialogAvailable() 或 resolveIpcRenderer() 判断可用性。

章节来源

  • osCtl.js:1-169
  • ipcRenderer.ts:1-51

结论

当前仓库的安全模型处于过渡态:bridge 已经定义了最小权限暴露方向,但默认配置仍未启用 contextIsolation,前端也保留了 require('electron') 的回退路径。对于新增能力,应遵循以下原则:

  • 所有系统能力放在主进程控制器。
  • 只通过 IPC 暴露必要方法。
  • bridge 只暴露 ipcRenderer,不暴露任何 Node/Electron 内部对象。
  • 前端统一通过 ipcRenderer.ts 调用,避免散落的 IPC 字符串。
  • 在未来启用 contextIsolation 前,先统一窗口配置,避免主窗口与智能工作台行为不一致。

[本节为总结性内容,不直接分析具体文件]

附录:新增安全 IPC 接口步骤

第一步:在主进程控制器中添加方法

新增一个安全方法到 osCtl.js,例如新增一个查询磁盘信息的接口。该方法应:

  • 接收有限参数
  • 在主进程侧执行系统调用
  • 捕获异常
  • 返回 { success, data | message }

参考现有方法风格:minimizeWindow、readBinaryFileRange、setMapLocalPath、uploadQingJianFiles。

章节来源

  • osCtl.js:1-169

第二步:确保 preload 已初始化相关服务

如果新能力依赖托盘、安全或其他服务,应在 preload/index.ts 的 preload() 中完成初始化。当前已初始化 trayService 和 securityService。

章节来源

  • index.ts:1-16

第三步:保持 bridge 暴露最小权限

不要直接把新方法写进 bridge.ts。bridge 继续只暴露 ipcRenderer,让前端通过 IPC 调用主进程控制器。

章节来源

  • bridge.ts:1-10

第四步:前端通过统一封装调用

前端调用新接口时,使用 ipcRenderer.ts 的 invokeIpc,通道形如 controller/osCtl/newMethod。不要在业务组件里直接拼接 IPC 字符串。

如果需要在组件中封装窗口相关交互,可参考 useElectronWindow.ts 的模式,把方法名集中管理,并处理失败情况。

章节来源

  • ipcRenderer.ts:1-51
  • useElectronWindow.ts:1-87

第五步:如需启用 contextIsolation

若要正式启用 contextIsolation,需要:

  1. 修改 windowOptions.ts,把 contextIsolation 改为 true,并根据需要调整 nodeIntegration。
  2. 在 config.default.ts 中确保主窗口和智能工作台窗口使用相同的 webPreferences。
  3. 确保 preload 路径指向编译后的 bridge 文件。
  4. 验证前端通过 window.electron.ipcRenderer 仍可调用 IPC。
  5. 逐步移除对 require('electron') 的直接依赖。

章节来源

  • windowOptions.ts:1-41
  • config.default.ts:1-50
  • bridge.ts:1-10
  • ipcRenderer.ts:1-51