Electron 桌面端安全基础

了解 Electron 常见安全配置,学会限制页面、导航和 IPC 的权限边界。

🎯 引言

Electron 页面同时拥有浏览器页面和桌面应用的可能能力。权限越大,页面受到恶意内容影响时风险越高,所以我们要给页面一把“只开必要房间的钥匙”。

学完本篇,你将能够:

  • 说清 contextIsolationsandboxnodeIntegration 的作用。
  • 通过 preload 暴露小而明确的接口。
  • 校验 IPC 参数,限制页面导航和新窗口打开。
  • 为页面添加基础 CSP。

🧱 三个重要配置

创建窗口时,先使用下面这组适合入门项目的配置:

electron/main.js
const win = new BrowserWindow({
    webPreferences: {
        preload: path.join(__dirname, 'preload.js'),
        contextIsolation: true,
        sandbox: true,
        nodeIntegration: false,
    },
});
配置作用
contextIsolation页面脚本和 preload 使用不同的 JavaScript 环境
sandbox限制渲染进程直接使用系统能力
nodeIntegration: false不让页面直接使用 Node.js 模块

这些配置不是替代 IPC,而是给 IPC 划出边界。页面仍然可以通过 preload 使用经过筛选的功能。


🛡 只暴露用途明确的接口

不要把完整的 ipcRenderer 暴露给页面:

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

contextBridge.exposeInMainWorld('electronAPI', {
    getAppVersion: () => ipcRenderer.invoke('app:get-version'),
});

页面只能调用 getAppVersion,不能自行拼接任意 IPC 频道。主进程还要检查参数,不能只相信页面传来的数据。

electron/main.js
ipcMain.handle('file:read-text', async (_event, filePath) => {
    if (typeof filePath !== 'string' || !filePath.endsWith('.txt')) {
        throw new Error('只允许读取文本文件');
    }

    return readFile(filePath, 'utf8');
});
文件路径、URL 和用户输入都要在主进程再次校验。渲染进程传来的参数不能当作可信数据。

🚫 限制导航和新窗口

Electron 页面可能显示服务端返回的内容、用户导入的 Markdown 或富文本。这些内容中的链接不一定由桌面端代码直接控制。

如果窗口跳到了外部网页,原来的应用界面就会被替换。外部网页也不应该直接运行在桌面应用的窗口环境里。

因此先规定一个简单规则:Electron 窗口只显示自己的页面,外部链接不在 Electron 中打开。

下面的示例假设打包后的应用通过 loadFile 加载本地页面:

electron/main.js
win.webContents.on('will-navigate', (event, url) => {
    if (!url.startsWith('file:')) {
        event.preventDefault();
    }
});

will-navigate 会在当前窗口准备跳转时触发。只要目标地址不是应用自己的 file: 页面,就取消这次跳转。

页面还可能通过 window.open()target="_blank" 请求新窗口。比如页面中有这样一个链接:

index.html
<a href="https://www.electronjs.org/" target="_blank">
    查看 Electron 文档
</a>

点击它时,Electron 会触发 setWindowOpenHandler。我们可以把 HTTPS 页面交给系统默认浏览器,再禁止 Electron 创建自己的新窗口:

electron/main.js
const { shell } = require('electron/main');

win.webContents.setWindowOpenHandler(({ url }) => {
    if (url.startsWith('https://')) {
        shell.openExternal(url);
    }

    return { action: 'deny' };
});

这段代码的执行过程是:

  1. 用户点击 target="_blank" 链接。
  2. setWindowOpenHandler 收到准备打开的新窗口地址。
  3. shell.openExternal(url) 调用系统默认浏览器打开 HTTPS 页面。
  4. { action: 'deny' } 告诉 Electron:不要再创建新的 Electron 窗口。

所以,shell.openExternal() 不是自动执行的 API,而是我们在确认地址符合要求后主动调用的“交给系统浏览器打开”方法。

开发环境使用 Vite 时,允许的地址还要包含 http://localhost:5173。这只是开发地址,打包后不要继续依赖它。


🧾 小节总结

  • 页面默认不应该直接使用 Node.js 或 Electron 模块。
  • preload 只暴露完成具体任务的方法。
  • IPC 参数要在主进程校验。
  • 应用应限制页面导航和新窗口来源。
  • CSP 可以减少页面加载未知脚本的机会。

❓ 知识问答

Q1:contextIsolation 开启后还能使用 preload 吗?

A:可以。preload 通过 contextBridge 暴露有限 API,正是隔离环境下的常用做法。

Q2:为什么不能把 ipcRenderer 整个暴露出去?

A:页面就可以调用任意频道,接口边界会失去意义,也更难校验参数。

🧪 小练习

把下面的接口改成只允许调用固定的 app:get-version 频道:

contextBridge.exposeInMainWorld('electronAPI', {
    // 请在这里编写代码
});

🎉 现在你已经知道如何给 Electron 页面设置基本安全边界了。