You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()判断本地是否存在用户信息,从未执行回调处理逻辑,自然不会触发对应事件。
修复方案
  1. 将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。

  2. 给AuthService补充回调处理的公开方法:

    // 在AuthService类内部添加
    public signinRedirectCallback(): Promise<User> {
      return this.userManager.signinRedirectCallback();
    }
    
  3. 在路由守卫中增加登录回调分支,跳回应用时优先处理授权回调,触发事件派发:

    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;
    
  4. 修改登出按钮的实例获取逻辑,使用单例:

    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 16:12:21