求Identity Server 4对接Angular Electron应用的AppAuthJs认证示例
AppAuth-JS + Angular Electron + IdentityServer4 实现指南
我太懂你这种在Electron环境下踩OIDC认证坑的感受了——之前Web端用oidc-client顺风顺水,到Electron里直接歇菜,毕竟Electron的混合环境(主进程+渲染进程+本地窗口)确实会给传统的OIDC流程带来不少特殊要求。下面我给你整理一套亲测可行的AppAuth-JS实现方案,涵盖核心步骤和关键代码片段:
核心前置说明
Electron里用AppAuth-JS的关键是处理认证窗口的生命周期和主/渲染进程间的状态传递,因为不能像Web那样直接在当前页面跳转认证页面,得单独开一个Electron窗口来处理IdentityServer4的登录流程。
步骤1:安装依赖
先把必要的包装上:
npm install @openid/appauth --save npm install electron --save-dev
步骤2:主进程(main.js)配置认证窗口
在Electron主进程里,我们需要监听渲染进程的认证请求,创建独立的登录窗口,并在窗口关闭时把认证结果传回渲染进程:
const { app, BrowserWindow, ipcMain } = require('electron'); let authWindow = null; let mainWindow = null; // 创建主应用窗口 function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false } }); mainWindow.loadURL('http://localhost:4200'); // Angular开发环境地址,打包后替换为本地html路径 } app.whenReady().then(() => { createWindow(); // 注册自定义协议,用于认证回调 app.setAsDefaultProtocolClient('com.your-electron-app'); }); // 监听渲染进程的认证请求 ipcMain.on('start-auth', async (event, authParams) => { if (authWindow) return; // 创建认证窗口 authWindow = new BrowserWindow({ width: 450, height: 600, show: false, webPreferences: { nodeIntegration: false, contextIsolation: true } }); // 加载IdentityServer4的授权页面 authWindow.loadURL(authParams.authUrl); authWindow.once('ready-to-show', () => { authWindow.show(); }); // 监听窗口URL变化,捕获认证回调 authWindow.webContents.on('will-navigate', (event, url) => { handleAuthCallback(url, event, authParams.redirectUri); }); authWindow.webContents.on('did-get-redirect-request', (event, oldUrl, newUrl) => { handleAuthCallback(newUrl, event, authParams.redirectUri); }); // 窗口关闭后清理 authWindow.on('closed', () => { authWindow = null; }); }); // 处理认证回调逻辑 function handleAuthCallback(url, event, redirectUri) { if (url.startsWith(redirectUri)) { event.preventDefault(); // 把回调URL传回Angular渲染进程 mainWindow.webContents.send('auth-callback', url); authWindow.close(); } }
步骤3:Angular端实现认证服务(AuthService)
在Angular里创建一个AuthService,封装AppAuth-JS的逻辑,同时和主进程通信:
import { Injectable } from '@angular/core'; import { AuthorizationService, AuthorizationRequest, TokenRequest, RedirectRequest } from '@openid/appauth'; import { ipcRenderer } from 'electron'; @Injectable({ providedIn: 'root' }) export class AuthService { private authService: AuthorizationService; // 替换成你的IdentityServer4配置 private authConfig = { issuer: 'https://your-identityserver4-domain', clientId: 'electron-app-client-id', redirectUri: 'com.your-electron-app://callback', postLogoutRedirectUri: 'com.your-electron-app://logout', scope: 'openid profile email your-api-scope' }; constructor() { this.authService = new AuthorizationService(); // 监听主进程传回的认证回调 ipcRenderer.on('auth-callback', (event, callbackUrl) => { this.processAuthResponse(callbackUrl); }); } // 发起登录请求 async login() { // 自动发现IdentityServer4的配置 const issuerMetadata = await this.authService.discover(this.authConfig.issuer); // 构建授权请求 const authRequest = new AuthorizationRequest({ client_id: this.authConfig.clientId, redirect_uri: this.authConfig.redirectUri, scope: this.authConfig.scope, response_type: AuthorizationRequest.RESPONSE_TYPE_CODE, state: this.generateRandomState() }); // 生成认证URL,传给主进程打开窗口 const authUrl = this.authService.createAuthorizationUrl(issuerMetadata, authRequest); ipcRenderer.send('start-auth', { authUrl, redirectUri: this.authConfig.redirectUri }); } // 处理认证回调,交换Token private async processAuthResponse(callbackUrl: string) { const issuerMetadata = await this.authService.discover(this.authConfig.issuer); const authResponse = await this.authService.makeAuthorizationRequest(callbackUrl, issuerMetadata); if (authResponse.code) { // 用授权码交换Access Token和ID Token const tokenRequest = new TokenRequest({ client_id: this.authConfig.clientId, redirect_uri: this.authConfig.redirectUri, grant_type: TokenRequest.GRANT_TYPE_AUTHORIZATION_CODE, code: authResponse.code, code_verifier: authResponse.code_verifier // AppAuth-JS自动生成的PKCE验证器 }); const tokenResponse = await this.authService.performTokenRequest(issuerMetadata, tokenRequest); // 建议用安全存储(如electron-store/keytar)存储Token,这里用localStorage做示例 localStorage.setItem('access_token', tokenResponse.accessToken); localStorage.setItem('id_token', tokenResponse.idToken); localStorage.setItem('refresh_token', tokenResponse.refreshToken); } } // 发起登出请求 async logout() { const issuerMetadata = await this.authService.discover(this.authConfig.issuer); const endSessionRequest = new RedirectRequest({ id_token_hint: localStorage.getItem('id_token'), post_logout_redirect_uri: this.authConfig.postLogoutRedirectUri }); const logoutUrl = this.authService.createEndSessionUrl(issuerMetadata, endSessionRequest); ipcRenderer.send('start-auth', { authUrl: logoutUrl, redirectUri: this.authConfig.postLogoutRedirectUri }); // 清空本地存储 localStorage.clear(); } // 获取当前Access Token getAccessToken(): string | null { return localStorage.getItem('access_token'); } // 生成随机state值,用于防止CSRF攻击 private generateRandomState(): string { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } }
步骤4:关键配置注意事项
- IdentityServer4客户端配置:
- 确保客户端启用
Authorization Code流程 - 开启PKCE支持(AppAuth-JS默认启用,必须开启)
- 在
Redirect URIs和Post Logout Redirect URIs中添加你自定义的协议地址(如com.your-electron-app://callback)
- 确保客户端启用
- Electron打包注意:打包后要确保自定义协议已正确注册,不同平台的打包工具(如electron-builder)需要在配置文件中声明协议
- 安全建议:不要硬编码敏感信息,Token存储优先使用加密存储方案(如
keytar)而非localStorage
常见坑点排查
- 认证窗口白屏:检查IdentityServer4的CORS配置,允许Electron窗口的访问来源
- 回调无响应:确认自定义协议已注册成功,且IdentityServer4中的回调地址与代码完全一致
- Token交换失败:检查客户端是否开启PKCE,以及授权请求中的
code_verifier是否正确传递
内容的提问来源于stack exchange,提问作者smoss
相关产品推荐
相关产品推荐

