Skip to main content

上下文隔离

🌐 Context Isolation

它是什么?

🌐 What is it?

上下文隔离是一项功能,它确保你的 preload 脚本和 Electron 的内部逻辑在与你在 webContents 中加载的网站分开的上下文中运行。这对于安全性非常重要,因为它有助于防止网站访问 Electron 内部或你的预加载脚本可以访问的强大 API。

🌐 Context Isolation is a feature that ensures that both your preload scripts and Electron's internal logic run in a separate context to the website you load in a webContents. This is important for security purposes as it helps prevent the website from accessing Electron internals or the powerful APIs your preload script has access to.

这意味着你的预加载脚本可以访问的 window 对象实际上与网站可以访问的对象是不同的。例如,如果你在预加载脚本中设置了 window.hello = 'wave',并且启用了上下文隔离,当网站尝试访问时,window.hello 将是未定义的。

🌐 This means that the window object that your preload script has access to is actually a different object than the website would have access to. For example, if you set window.hello = 'wave' in your preload script and context isolation is enabled, window.hello will be undefined if the website tries to access it.

自 Electron 12 起,默认启用了上下文隔离,这是对所有应用都推荐的安全设置。

🌐 Context isolation has been enabled by default since Electron 12, and it is a recommended security setting for all applications.

迁移

🌐 Migration

没有上下文隔离时,我曾经使用 window.X = apiObject 从预加载脚本提供 API。现在怎么办?

之前:上下文隔离已禁用

🌐 Before: context isolation disabled

在渲染进程中从预加载脚本向已加载的网站公开 API 是一种常见用例。在禁用上下文隔离的情况下,你的预加载脚本会与渲染器共享一个公共全局 window 对象。然后你可以将任意属性附加到预加载脚本上:

🌐 Exposing APIs from your preload script to a loaded website in the renderer process is a common use-case. With context isolation disabled, your preload script would share a common global window object with the renderer. You could then attach arbitrary properties to a preload script:

preload.js
// preload with contextIsolation disabled
window.myAPI = {
doAThing: () => {}
}

doAThing() 函数随后可以直接在渲染进程中使用:

🌐 The doAThing() function could then be used directly in the renderer process:

renderer.js
// use the exposed API in the renderer
window.myAPI.doAThing()

之后:已启用上下文隔离

🌐 After: context isolation enabled

在 Electron 中有一个专门的模块可以帮助你轻松实现这一点。contextBridge 模块可以安全地将预加载脚本的隔离上下文中的 API 暴露到网站运行的上下文中。网站仍然可以像之前一样通过 window.myAPI 访问这些 API。

🌐 There is a dedicated module in Electron to help you do this in a painless way. The contextBridge module can be used to safely expose APIs from your preload script's isolated context to the context the website is running in. The API will also be accessible from the website on window.myAPI just like it was before.

preload.js
// preload with contextIsolation enabled
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
doAThing: () => {}
})
renderer.js
// use the exposed API in the renderer
window.myAPI.doAThing()

请阅读上方链接的 contextBridge 文档,以充分了解其限制。例如,你不能通过桥接发送自定义原型或符号。

🌐 Please read the contextBridge documentation linked above to fully understand its limitations. For instance, you can't send custom prototypes or symbols over the bridge.

安全考虑

🌐 Security considerations

仅仅启用 contextIsolation 并使用 contextBridge 并不意味着你所做的一切都是安全的。例如,这段代码是 不安全的

🌐 Just enabling contextIsolation and using contextBridge does not automatically mean that everything you do is safe. For instance, this code is unsafe.

preload.js
// ❌ Bad code
contextBridge.exposeInMainWorld('myAPI', {
send: ipcRenderer.send
})

它直接暴露了一个强大的 API,而没有任何形式的参数过滤。这将允许任何网站发送任意的 IPC 消息,而你并不希望这种情况发生。暴露基于 IPC 的 API 的正确方式应该是为每个 IPC 消息提供一个方法。

🌐 It directly exposes a powerful API without any kind of argument filtering. This would allow any website to send arbitrary IPC messages, which you do not want to be possible. The correct way to expose IPC-based APIs would instead be to provide one method per IPC message.

preload.js
// ✅ Good code
contextBridge.exposeInMainWorld('myAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})

与 TypeScript 一起使用

🌐 Usage with TypeScript

如果你正在使用 TypeScript 构建 Electron 应用,你会希望为通过上下文桥暴露的 API 添加类型。除非你用 声明文件 扩展类型,否则渲染器的 window 对象不会有正确的类型定义。

🌐 If you're building your Electron app with TypeScript, you'll want to add types to your APIs exposed over the context bridge. The renderer's window object won't have the correct typings unless you extend the types with a declaration file.

例如,给定这个 preload.ts 脚本:

🌐 For example, given this preload.ts script:

preload.ts
contextBridge.exposeInMainWorld('electronAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})

你可以创建一个 interface.d.ts 声明文件,并全局扩展 Window 接口:

🌐 You can create a interface.d.ts declaration file and globally augment the Window interface:

interface.d.ts
export interface IElectronAPI {
loadPreferences: () => Promise<void>,
}

declare global {
interface Window {
electronAPI: IElectronAPI
}
}

这样做可以确保在渲染进程中编写脚本时,TypeScript 编译器会知道全局 window 对象上的 electronAPI 属性:

🌐 Doing so will ensure that the TypeScript compiler will know about the electronAPI property on your global window object when writing scripts in your renderer process:

renderer.ts
window.electronAPI.loadPreferences()