Electron 集成
将 Better Auth 集成到 Electron 中。
Electron 是一个流行的框架,使用 Web 技术构建跨平台的桌面应用程序。
Better Auth 可以集成到 Electron 应用中,提供安全的认证流程,利用系统浏览器。
安装
配置 Better Auth 前端和后端
在与 Electron 集成之前,请确保您已经设置好了 Better Auth 服务器和客户端。
要开始,请查看我们的 安装指南 来设置 Better Auth。
安装所需包
在您的服务器、Electron 应用和 Web 客户端项目中安装 Better Auth 服务器包和 Electron 集成包。
在每个项目中运行:
npm install better-auth @better-auth/electron我们支持比最新稳定版本的 Electron 主版本晚两个主版本。 这样能让您使用获得安全更新的版本,且符合 Electron 的版本支持策略。
向 Better Auth 服务器添加 Electron 插件
将 Electron 插件添加到您的 Better Auth 服务器。
import { betterAuth } from "better-auth";
import { electron } from "@better-auth/electron";
export const auth = betterAuth({
plugins: [electron()],
emailAndPassword: {
enabled: true,
},
social: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
});在 Web 客户端添加代理插件
在前端添加代理插件,以处理重定向回 Electron 应用。
import { createAuthClient } from "better-auth/client";
import { electronProxyClient } from "@better-auth/electron/proxy";
export const authClient = createAuthClient({
baseURL: "http://localhost:8081",
plugins: [
electronProxyClient({
protocol: {
scheme: "com.example.app"
},
}),
],
});初始化 Electron 客户端
import { createAuthClient } from "better-auth/client";
import { electronClient } from "@better-auth/electron/client";
export const authClient = createAuthClient({
baseURL: "http://localhost:8081", // 前端 Better Auth 的基础 URL
plugins: [
electronClient({
signInURL: "https://app.example.com/sign-in", // 认证重定向的 URL
protocol: {
scheme: "com.example.app" // Electron 应用注册的自定义协议
},
storage: {
getItem: async (key) => {
// 从存储中检索条目
},
setItem: async (key, value) => {
// 在存储中设置条目
},
},
}),
],
});如果您更愿意使用默认的存储方案,可以这样做:
- 确保已安装
conf包:
npm install conf- 然后可以按如下方式使用:
import { storage } from "@better-auth/electron/storage";
electronClient({
storage: storage(),
});您不应直接将 authClient 暴露给渲染进程。相反,请创建 IPC 桥以在主进程和渲染进程之间进行安全通信。
同时,请确保不要向渲染进程暴露任何敏感数据(如令牌或 Cookie),以缓解注入攻击。
协议和可信来源
Electron 插件使用深度链接在认证后将用户重定向回您的应用。
为此,您需要在 Better Auth 服务器 的 trustedOrigins 中添加应用的协议方案。
首先,请确保在构建配置中注册了自定义协议方案。
module.exports = {
packagerConfig: {
protocols: [{
name: "MyApp Protocol",
schemes: ["com.example.app"],
}],
},
// ...其他配置选项
};然后,更新 Better Auth 配置以在 trustedOrigins 中包含该方案:
export const auth = betterAuth({
trustedOrigins: ["com.example.app:/"],
});配置 BrowserWindow
确保您的 BrowserWindow 配置中 nodeIntegration 设置为 false,contextIsolation 设置为 true,以确保 NodeJS API 不会暴露给在浏览器中运行的任何 JavaScript 进程。
import { BrowserWindow } from "electron";
import { join } from "node:path";
const win = new BrowserWindow({
webPreferences: {
preload: join(__dirname, "preload.mjs"),
nodeIntegration: false,
contextIsolation: true,
},
});如果要在 sandbox 模式下运行进程,请确保将 @better-auth/electron 打包到预加载脚本中。
仅在预加载脚本中导入 @better-auth/electron/preload,并启用 tree-shaking,以避免打包不必要的依赖项。
以下示例使用 electron-vite,但该概念适用于任何打包工具:
import { defineConfig } from "electron-vite";
export default defineConfig({
preload: {
build: {
externalizeDeps: {
exclude: ["@better-auth/electron"],
},
},
},
});如果配置使用显式外部依赖列表,请不要将 @better-auth/electron 添加到该列表中,以便将其打包进去。
设置主进程
在 Electron 主进程中,使用 Electron 认证客户端的 setupMain() 方法来处理必要的配置。
这将:
- 注册深度链接的协议处理器。
- 注册 用户图像代理 协议。
- 设置内容安全策略。
- 设置主进程与渲染进程之间的 IPC 桥。
import { authClient } from "./lib/auth-client";
authClient.setupMain();请注意,这必须在应用准备就绪之前调用。
设置渲染进程
在预加载脚本中,使用 Electron 认证客户端的 setupRenderer() 方法向渲染进程公开安全的 IPC 桥。
import { setupRenderer } from "@better-auth/electron/preload";
setupRenderer();请注意,这也必须在应用准备就绪之前调用。
要推断公开桥的类型,可以使用 authClient.$Infer.Bridges 来扩展 Window 接口。
import type { authClient } from "./lib/auth-client";
declare global {
type Bridges = typeof authClient.$Infer.Bridges;
interface Window extends Bridges {}
}用法
浏览器中的授权处理
为了将用户重定向回您的 Electron 应用,您需要在 Web 的登录回调页面调用 ensureElectronRedirect() 方法。同时,请确保在发起登录请求时通过 fetchOptions.query 保留 PKCE 和 state 参数。
以下示例使用 React,但逻辑适用于任意框架:
import { useEffect, use } from "react";
import { authClient } from "../auth-client";
function SignIn({
searchParams,
}: {
searchParams: Promise<{
client_id?: string | undefined;
state?: string | undefined;
code_challenge?: string | undefined;
}>;
}) {
const query = use(searchParams);
useEffect(() => {
const id = authClient.ensureElectronRedirect();
return () => {
clearTimeout(id);
};
}, []);
return (
<button
onClick={() =>
authClient.signIn.social({
provider: "google",
fetchOptions: { query }, // preserve PKCE/state
})
}
>
使用 Google 登录
</button>
);
}要处理已登录用户,可以使用 electron.transferUser 方法在客户端和 electronTransferUser 在服务器端将用户转移到 Electron 应用中。参见示例
Electron 中的身份验证处理
在 Electron 的渲染进程中,您可以使用预加载脚本暴露的 IPC 桥:
function Auth() {
useEffect(() => {
const unsubscribeAuthenticated =
window.onAuthenticated((user) => {
console.log("认证用户:", user);
});
const unsubscribeAuthError =
window.onAuthError((ctx) => {
toast.error(`认证错误: ${ctx.message}`);
});
return () => {
unsubscribeAuthenticated();
unsubscribeAuthError();
};
}, []);
return (
<>
<button onClick={() => window.requestAuth()}>
使用浏览器登录
</button>
<button
onClick={() =>
window.requestAuth({
provider: "google", // sign in with social provider, redirecting directly to the provider
})
}
>
使用 Google 登录
</button>
</>
);
}在主进程中,您可以直接调用 auth client:
import { authClient } from "./lib/auth-client";
authClient.requestAuth();登出
在渲染进程中,可以使用 signOut 桥退出登录:
<button onClick={() => window.signOut()}>退出登录</button>订阅用户更新
可以通过渲染进程的 onUserUpdated 桥监听用户信息变更:
useEffect(() => {
const unsubscribe = window.onUserUpdated((user) => {
console.log("用户更新:", user);
});
return () => unsubscribe();
}, []);错误处理
通过渲染进程的 onAuthError 桥监听认证错误。该桥会接收由 fetch 钩子转发的错误上下文。
useEffect(() => {
const unsubscribe = window.onAuthError((ctx) => {
console.error("认证错误:", ctx);
});
return () => unsubscribe();
}, []);手动令牌交换
在某些环境中,深度链接跳转可能不稳定(例如某些 Linux 桌面环境、沙箱浏览器,或者 Electron 应用未注册为该协议的默认处理程序)。 作为备选方案,用户可以从 Web 界面手动复制授权码并粘贴到 Electron 应用中。
前端:
认证完成后,可以通过代理客户端的 electron.getAuthorizationCode() 方法获取授权码。您可以将该码展示给用户以便复制:
import { useEffect } from "react";
import { authClient } from "../auth-client";
function Providers({ children }) {
useEffect(() => {
const authorizationCode = authClient.electron.getAuthorizationCode();
if (authorizationCode) {
// 向用户展示授权码
console.log("授权码:", authorizationCode);
}
}, []);
return <>{children}</>;
}授权码也会在 Electron 应用中的登录发起后返回,或在使用 transferUser 方法时返回。
const { data } = await authClient.electron.transferUser({
fetchOptions: {
query: params,
},
});
if (data?.electron_authorization_code) {
// 向用户展示授权码
}Electron:
在 Electron 渲染进程中,提供输入框供用户粘贴授权码,然后调用 window.authenticate() 交换会话:
function ManualCodeEntry() {
return (
<input
type="text"
placeholder="请粘贴授权码"
maxLength={32}
onChange={(e) => {
if (e.target.value.length === 32) {
// 重要:必须先调用过 requestAuth()
window.authenticate({ token: e.target.value });
}
}}
/>
);
}authenticate 桥会将授权码发送到主进程,主进程使用存储的 PKCE 验证码和 state 进行交换。成功时,会触发 onAuthenticated 桥。
授权码是一个短时有效的 32 字符字符串。必须先调用 requestAuth(),因为其生成的验证码和 state 是 authenticate() 所依赖的。
用户头像代理
为避免 CSP 问题,Electron 插件通过自定义协议 user-image:// 安全代理用户头像图片。
您可以直接在渲染进程中使用图片 URL:
<img src={user.image} alt="头像" />
{/* 或 <img src="user-image://<user-id>" /> */}要配置或禁用代理,请参见 userImageProxy 选项。
若要访问除当前用户外其他用户的头像,请启用 管理员插件。
创建 IPC 桥
您应该创建 IPC 桥以扩展渲染进程暴露的功能,确保 API 表面尽可能小且安全。
首先,在主进程中创建 IPC 处理器,使用 authClient 执行相应操作:
import { authClient } from "./lib/auth-client";
import { ipcMain } from "electron";
ipcMain.handle("myBridge", async (_event, data) => {
const cookie = authClient.getCookie();
return await authClient.someEndpoint({
data,
fetchOptions: {
headers: { cookie },
},
});
});接着,在预加载脚本中使用 contextBridge 暴露桥:
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("myBridge", (data: Record<string, any>) => {
return ipcRenderer.invoke("myBridge", data);
});要推断您桥的类型,可以扩展 Window 接口:
import type { authClient } from "./lib/auth-client";
declare global {
type Bridges = typeof authClient.$Infer.Bridges;
interface Window extends Bridges {
myBridge: (data: Record<string, any>) => Promise<any>;
}
}现在您可以在渲染进程的任何位置调用自定义桥:
useEffect(() => {
window
.myBridge({
foo: "bar",
})
.then((res) => {
console.log("桥响应:", res);
});
}, []);更多详细信息请参考 Electron 的进程间通信(IPC)教程。
选项
服务器插件
codeExpiresIn?
授权码有效时长(秒)。默认 300
注意,授权码在活跃的端点使用中会刷新。
electron({
codeExpiresIn: 300, // 5 分钟
});redirectCookieExpiresIn?
重定向 cookie 有效时长(秒)。默认 120
electron({
redirectCookieExpiresIn: 120, // 2 分钟
});重定向 cookie 名称由 clientID 派生。
cookiePrefix?
插件设置的 cookie 前缀。默认 better-auth
electron({
cookiePrefix: "better-auth",
});clientID?
用于识别 Electron 客户端的 client id。默认 electron
确保与代理客户端和 electron 客户端插件使用的 clientID 一致。
electron({
clientID: "electron",
});代理客户端
protocol
Electron 深度链接使用的协议方案。
应遵循 反向域名命名规范 保证唯一性。
确保与 Electron 客户端和 trustedOrigins 使用的协议一致。
electronProxyClient({
protocol: "com.example.app",
});electronProxyClient({
protocol: {
scheme: "com.example.app",
},
});callbackPath?
认证重定向使用的回调路径。默认 /auth/callback
确保与 electron 客户端插件路径一致。
electronProxyClient({
callbackPath: "/auth/callback",
});clientID?
用于识别 Electron 客户端的 client id。默认 electron
确保与服务器和 electron 客户端插件的 clientID 一致。
electronProxyClient({
clientID: "electron",
});cookiePrefix?
插件设置的 cookie 前缀。默认 better-auth
electronProxyClient({
cookiePrefix: "better-auth",
});客户端插件
signInURL
用于认证跳转的 URL。
electronClient({
signInURL: "http://localhost:3000/sign-in",
});protocol
Electron 深度链接使用的协议方案。
应遵循 反向域名命名规范 保证唯一性。
确保与代理客户端和 trustedOrigins 的协议一致。
electronProxyClient({
protocol: "com.example.app",
});electronProxyClient({
protocol: {
scheme: "com.example.app",
},
});callbackPath?
认证重定向使用的回调路径。默认 /auth/callback
确保与代理客户端插件路径一致。
electronClient({
callbackPath: "/auth/callback",
});storage
用于存储会话和 cookie 数据的存储方案。
默认是生成一个放在 userData 目录的存储文件,文件名由项目名派生。
electronClient({
storage: {
getItem: (key) => {
// 从存储读取条目
},
setItem: (key, value) => {
// 在存储写入条目
},
},
});storagePrefix?
本地存储键的前缀。默认 better-auth
electronClient({
storagePrefix: "better-auth",
});cookiePrefix?
服务器 cookie 名称的前缀(数组)。默认 better-auth
用于识别哪类 cookie 属于 better-auth,防止第三方 cookie 引起无限重新请求。
electronClient({
cookiePrefix: "better-auth",
});electronClient({
cookiePrefix: ["better-auth", "my-app"],
});channelPrefix?
IPC 桥通信通道的前缀。默认 better-auth
electronClient({
channelPrefix: "myapp",
});clientID?
用于识别 Electron 客户端的 client id。默认 electron
确保和服务器及代理客户端插件中的 clientID 一致。
electronClient({
clientID: "electron",
});sanitizeUser?
发送给渲染进程前对用户对象做 sanitize 的函数。用于剥离敏感字段。
electronClient({
sanitizeUser: (user) => {
const { sensitiveField, ...rest } = user;
return rest;
},
});userImageProxy?
用户头像代理的配置。详见 用户头像代理。
electronClient({
userImageProxy: {
enabled: true, // 默认开启
maxSize: 1024 * 1024 * 5, // 默认 5MB
},
});若要完全禁用代理:
electronClient({
userImageProxy: {
enabled: false,
},
});disableCache?
是否禁用会话数据的本地缓存。默认 false
electronClient({
disableCache: true,
});