Vue3集成oidc-client-ts时UserManager事件未触发问题咨询
问题描述
开发Vue3单页应用时,使用oidc-client-ts库实现身份认证,目前登录、登出核心流程可正常运行,但无法监听UserManager提供的addUserLoaded、addUserSignedIn事件,即使用户完成登出后重新登录的全流程,已注册的事件回调也始终不会触发。
业务流程:用户访问应用域名时,会被重定向到身份服务器,登录成功后再跳转回应用页面。应用初始化时会先拉取config.json配置,存储到window.appConfig作为UserManager的初始化参数。
原有问题代码
应用启动逻辑
fetch("config.json", {'method': "GET"}) .then(response => { if(response.ok) { response.json().then(appConfigData => { window.appConfig = appConfigData; createApp(App) .use(store) .use(router) .use(AccordionPlugin) .component("font-awesome-icon", FontAwesomeIcon) .mount("#app"); }); } else{ alert("Server returned " + response.status + " : " + response.statusText); } });
AuthService封装
import { UserManager, WebStorageStateStore, User, UserManagerEvents } from "oidc-client-ts"; import jwt_decode from "jwt-decode"; import store from "@/store"; export default class AuthService { private userManager: UserManager; constructor(configuration: any) { const IDP_URL: string = configuration.IDP_URL; const REDIRECT_URI: string = configuration.REDIRECT_URI; const AUTH_TOKEN_URI: string = configuration.AUTH_TOKEN_URI; const CLIENT_ID: string = configuration.CLIENT_ID; const SILENT_RENEW_REDIRECT_URI: string = configuration.SILENT_RENEW_REDIRECT_URI; const POPUP_REDIRECT_URI: string = configuration.POPUP_REDIRECT_URI; const settings: any = { userStore: new WebStorageStateStore({ store: window.localStorage }), authority: IDP_URL, client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, popup_redirect_uri: POPUP_REDIRECT_URI, response_type: "code", automaticSilentRenew: true, silent_redirect_uri: SILENT_RENEW_REDIRECT_URI, scope: "openid profile ContactCenterApi.READ_WRITE", post_logout_redirect_uri: AUTH_TOKEN_URI, loadUserInfo: true }; this.userManager = new UserManager(settings); this.userManager.events.addUserLoaded(_args => { console.log("USER LOADED EVENT"); debugger; this.userManager.getUser().then(usr => { store.dispatch("getProducts", usr?.profile.sub) // load the users products }); }); this.userManager.events.addUserSignedIn(() => { console.log("USER SIGNED IN EVENT"); debugger; this.userManager.getUser().then(usr => { store.dispatch("getProducts", usr?.profile.sub) // load the users products }); }); } public getUser(): Promise<User|null> { return this.userManager.getUser(); } public async login(): Promise<void> { return this.userManager.signinRedirect(); } public logout(): Promise<void> { return this.userManager.signoutRedirect(); }
路由守卫逻辑
const auth = new AuthService(window.appConfig); router.beforeEach(async (to, from, next) => { const user = await auth.getUser(); const baseUri = window.appConfig.BASE_URI + "/#"; console.log("redirectedUri = " + baseUri + to.fullPath); if (user) { if (!user.expired) { next(); } else { sessionStorage.setItem("redirectedUri", baseUri + to.fullPath); await auth.login(); } } else { sessionStorage.setItem("redirectedUri", baseUri + to.fullPath); await auth.login(); } }); export default router;
登出按钮逻辑
import AuthService from "@/auth/AuthService"; onselect: async function(event: MenuEventArgs) { var authService = new AuthService(window.appConfig); // settings are stored in the window when the app mounts if(event.item.text === 'Logout') { await authService.logout(); } }
问题根因
- 重复实例化AuthService导致事件监听绑定失效:路由守卫、登出按钮逻辑中都手动
new AuthService(),每次实例化都会创建全新的UserManager对象,之前注册的事件回调仅绑定在对应旧实例上,新实例既没有注册回调,也无法接收旧实例的事件通知。 - 缺少登录回调处理逻辑,事件没有触发时机:oidc-client-ts的重定向登录流程中,从身份服务器跳回应用时,必须主动调用
signinRedirectCallback()方法解析URL中的授权参数、换取token、写入用户存储,addUserLoaded、addUserSignedIn事件只会在这个方法执行成功后派发。原有代码仅通过getUser()判断本地是否存在用户信息,从未执行回调处理逻辑,自然不会触发对应事件。
修复方案
将AuthService改为单例模式,全局仅保留一个UserManager实例,确保事件监听统一绑定在同一个实例上。
在AuthService.ts末尾添加单例获取方法:let authInstance: AuthService | null = null; export const getAuthService = (config: any) => { if (!authInstance) { authInstance = new AuthService(config); } return authInstance; };后续所有使用AuthService的位置(路由守卫、登出按钮等),都通过
getAuthService(window.appConfig)获取实例,禁止直接new。给AuthService补充回调处理的公开方法:
// 在AuthService类内部添加 public signinRedirectCallback(): Promise<User> { return this.userManager.signinRedirectCallback(); }在路由守卫中增加登录回调分支,跳回应用时优先处理授权回调,触发事件派发:
import { getAuthService } from "@/auth/AuthService"; const auth = getAuthService(window.appConfig); router.beforeEach(async (to, from, next) => { const baseUri = window.appConfig.BASE_URI + "/#"; // 识别从身份服务器跳回的回调请求 if (window.location.search.includes("code=") && window.location.search.includes("state=")) { try { // 处理回调:解析code换token、存储用户、触发addUserLoaded/addUserSignedIn事件 await auth.signinRedirectCallback(); // 跳转到登录前存储的目标路径,清除URL上的授权参数 const targetPath = sessionStorage.getItem("redirectedUri") || baseUri; sessionStorage.removeItem("redirectedUri"); next({ path: targetPath.replace(baseUri, "/") }); return; } catch (err) { console.error("登录回调处理失败", err); sessionStorage.setItem("redirectedUri", baseUri + to.fullPath); await auth.login(); return; } } const user = await auth.getUser(); if (user && !user.expired) { next(); } else { sessionStorage.setItem("redirectedUri", baseUri + to.fullPath); await auth.login(); } }); export default router;修改登出按钮的实例获取逻辑,使用单例:
import { getAuthService } from "@/auth/AuthService"; onselect: async function(event: MenuEventArgs) { const authService = getAuthService(window.appConfig); if(event.item.text === 'Logout') { await authService.logout(); } }
内容的提问来源于stack exchange,提问作者Logan Cooper
相关产品推荐
相关产品推荐

