如何为 Electron 应用添加身份验证
如果你曾搜索过如何为 Electron 应用添加身份验证,大概会找到一些教程,它们通过打开 BrowserWindow 来显示登录表单,在 Electron 内部拦截 OAuth 回调,并以明文形式存储令牌。这种方式已经过时、存在安全隐患,且与当今 OAuth 2.0 原生应用规范的实际工作方式相冲突。
本文将介绍正确的架构方案:带 PKCE 的授权码流程、通过系统浏览器登录,以及使用深度链接或回环重定向将令牌返回给应用。
核心要点
- 在嵌入式
BrowserWindow中执行登录流程违反了 RFC 8252,会使用户面临凭据被拦截的风险。请改用系统浏览器。 - PKCE(Proof Key for Code Exchange,代码交换证明密钥)取代了客户端密钥——Electron 应用无法安全地存储客户端密钥。
- 通过自定义 URI 方案的深度链接或回环重定向,可将授权码返回给主进程。
- 使用
safeStorage存储刷新令牌,该机制利用操作系统级别的密钥管理(Keychain、DPAPI、libsecret)。 - 通过设置
contextIsolation: true隔离渲染进程,并仅通过contextBridge暴露有限的 IPC 方法。
为什么 Electron 的身份验证与 Web 身份验证不同
Electron 应用运行在用户可控的机器上。应用没有后端来隐藏客户端密钥,也没有浏览器沙箱将应用与操作系统隔离。这从根本上改变了安全威胁模型。
RFC 8252 是专门针对原生和桌面应用编写的 OAuth 2.0 标准,其中明确规定:不得在 OAuth 登录流程中使用嵌入式 Web 视图。嵌入式 BrowserWindow 可以静默拦截用户凭据,且用户无法验证自己是否正在与合法的身份提供商通信。请改用系统浏览器。
正确架构:带深度链接的 OAuth 2.0 PKCE
推荐的流程如下:
- 用户在 Electron 应用中点击”登录”。
- 应用使用授权 URL 打开系统浏览器。
- 用户通过身份提供商(Auth0、Clerk、Firebase Authentication、AWS Cognito 或任何兼容 OpenID Connect 的提供商)完成身份验证。
- 提供商重定向至自定义 URI 方案(如
myapp://auth/callback)或回环地址(如http://127.0.0.1:PORT/callback)。 - Electron 主进程拦截该重定向,并使用授权码换取令牌。
- 使用
safeStorage安全存储令牌。
由于 Electron 应用无法安全保存客户端密钥,PKCE(Proof Key for Code Exchange) 完全取代了密钥的作用。在流程开始之前,应用会生成一个随机的 code_verifier,将其哈希为 code_challenge,并在授权请求中携带该挑战值。在用授权码换取令牌时,再发送原始的 code_verifier。授权服务器验证两者是否匹配。整个过程中,密钥从不离开你的应用。
为 OAuth 回调配置深度链接
在主进程中注册自定义协议处理器:
// main.js
const { app } = require('electron');
const path = require('path');
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('myapp', process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient('myapp');
}
// macOS:处理深度链接回调
app.on('open-url', (event, callbackUrl) => {
event.preventDefault();
handleAuthCallback(callbackUrl); // 解析授权码,换取令牌
});
// Windows 和 Linux:深度链接通过 second-instance 事件传递
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, argv) => {
const callbackUrl = argv.find((arg) => arg.startsWith('myapp://'));
if (callbackUrl) handleAuthCallback(callbackUrl);
});
}
在 Windows 和 Linux 上,second-instance 事件以命令行参数的形式传递深度链接。在 macOS 上,应使用 open-url 事件。在打包配置方面,需在 electron-builder 中配置协议:
{
"protocols": {
"name": "myapp",
"schemes": ["myapp"]
}
}
注意: 在 macOS 和 Linux 上,深度链接通常只在打包后的应用中才能可靠运行,因为协议处理器必须向操作系统注册。请务必针对打包后的构建版本测试深度链接,而不仅仅使用
electron .运行时测试。
Discover how at OpenReplay.com.
使用 Electron safeStorage 安全存储令牌
切勿将令牌存储在 localStorage、明文文件或未加密的 electron-store 中。请使用 safeStorage,它通过操作系统级别的密钥管理对数据进行加密(macOS 使用 Keychain,Windows 使用 DPAPI,Linux 使用 libsecret):
const { app, safeStorage } = require('electron');
const fs = require('fs');
const path = require('path');
const TOKEN_PATH = path.join(app.getPath('userData'), 'refresh.enc');
async function saveRefreshToken(token) {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error('Encryption is not available on this system');
}
const encrypted = await safeStorage.encryptStringAsync(token);
fs.writeFileSync(TOKEN_PATH, encrypted);
}
async function loadRefreshToken() {
if (!fs.existsSync(TOKEN_PATH)) return null;
const encrypted = fs.readFileSync(TOKEN_PATH);
return await safeStorage.decryptStringAsync(encrypted);
}
在加密之前,务必检查 safeStorage.isEncryptionAvailable(),在 Linux 上尤为重要,因为操作系统密钥环可能未配置。在某些 Linux 系统上,如果没有可用的安全密钥环,Electron 可能会回退到 basic_text 后端。你可以通过 safeStorage.getSelectedStorageBackend() 查看当前使用的后端,并在必要时向用户发出警告。
保持渲染进程隔离
渲染进程不应直接接触令牌。请使用 contextIsolation,将 nodeIntegration 设为 false,并仅通过 contextBridge 向渲染进程暴露必要的接口:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('auth', {
login: () => ipcRenderer.invoke('auth:login'),
logout: () => ipcRenderer.send('auth:logout'),
getProfile: () => ipcRenderer.invoke('auth:get-profile'),
});
主进程负责处理所有令牌操作,渲染进程只调用具名的 IPC 方法。这样即使渲染进程遭到恶意内容入侵,也能将影响范围降到最低。
选择身份提供商
上述架构适用于任何兼容 OpenID Connect 的提供商。Electron 应用常用的选择包括 Auth0、Clerk、Firebase Authentication 和 AWS Cognito。在所选提供商中,将应用注册为原生或桌面应用类型,并将自定义 URI 方案配置为允许的重定向 URI。
应避免的模式
许多教程仍在展示以下模式:
// ❌ 请勿这样做
const authWindow = new BrowserWindow({ /* ... */ });
authWindow.loadURL(authorizationUrl);
authWindow.webContents.session.webRequest.onBeforeRequest(
{ urls: ['http://localhost/callback*'] },
({ url }) => { /* 拦截令牌 */ }
);
这种方式将登录流程嵌入 Electron 内部,违反了 RFC 8252,且剥夺了用户验证身份提供商的能力。无论有多少教程仍在推荐这种做法,都应坚决避免。
结论
为 Electron 应用添加身份验证的正确方式其实并不复杂:打开系统浏览器,使用带 PKCE 的 OAuth 2.0,通过深度链接或回环重定向处理回调,并使用 safeStorage 存储令牌。同时,保持渲染进程与所有令牌逻辑的隔离。这一架构正是 RFC 8252 所推荐的方案,也是 Auth0、Clerk 等现代提供商在注册原生应用时所期望的实现方式。
常见问题
可以。回环重定向在 127.0.0.1 上启动一个使用随机端口的本地 HTTP 服务器来接收 OAuth 回调。这种方式无需进行操作系统级别的协议注册,在开发阶段非常实用。其代价是每次登录都需要启动和停止服务器,且某些提供商要求在控制台中显式配置回环 URI。
需要。即使提供商签发了客户端密钥,也不应将其嵌入 Electron 二进制文件中,因为用户可以将其提取出来。PKCE 保护的是授权码交换过程,与是否存在密钥无关。请将应用注册为原生或公共客户端,以强制要求使用 PKCE,并避免使用客户端密钥。
仅使用 safeStorage 存储刷新令牌。当访问令牌过期时,主进程将刷新令牌发送至提供商的令牌端点,并获取新的访问令牌。访问令牌应仅保存在主进程的内存中,绝不存入渲染进程。如果刷新令牌被吊销或已过期,则提示用户通过系统浏览器重新登录。
自定义协议处理器必须向操作系统注册,而这一注册过程通常在应用安装或打包时完成。直接运行 electron . 会启动 Electron 二进制文件,操作系统并不知道你的应用处理 myapp:// 方案。请务必针对由 electron-builder 或类似工具生成的打包构建版本测试深度链接。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.