使用 Vue 3 插件(Plugin)实现 OIDC 登录和修改密码(OIDC 系统以 Keycloak 为例)

  • 使用 Vue 3 插件(Plugin)实现 OIDC 登录和修改密码(OIDC 系统以 Keycloak 为例)已关闭评论
  • 88 次浏览
  • A+
所属分类:Web前端
摘要

目前单位系统常用 Keycloak 作为认证系统后端,而前端之前写的也比较随意,这次用 Vue 3 插件以及 Ref 响应式来编写这个模块。另外,这个可能是全网唯一使用 keycloak 的 OIDC 原生更新密码流的介绍代码。


背景

目前单位系统常用 Keycloak 作为认证系统后端,而前端之前写的也比较随意,这次用 Vue 3 插件以及 Ref 响应式来编写这个模块。另外,这个可能是全网唯一使用 keycloak 的 OIDC 原生更新密码流的介绍代码。

设计

依赖库选择

OIDC 客户端,这里选择 oidc-client-ts 来提供 OIDC 相关的服务,根据目前的调研这个算是功能比较齐全、兼容性比较好的 OIDC 客户端了。像 keycloak.js,其实也没有修改密码和自动刷新 token 的功能。另外像 Auth0 Vue SDK 则只能用于 Auth0,但他设计上还是不错的,也是通过 Vue 3 原生的插件功能实现的。

具体设计

根据 Vue 3 的官方插件文档,主要需要两部分组成,一个是需要定义一个 Plugin 并在里面使用 provide 来提供对象,另一个则是需要定义一个方法使用 inject 来接收提供的对象。

这里给原本的 oidc-client-ts 里的 UserManager 来个套娃,外层这个套一层,叫 AuthManager 。这样就可以将一些初始化时加载 LocalStorage 里的 token 等等逻辑封装在这里面,同时也可以对外暴露一些 Ref 让其他组件可以监听变化。

代码

废话不多说了,咱还是老样子,直接上代码

auth-manager.ts

import { UserManager, UserManagerSettings } from 'oidc-client-ts'; import { Plugin, inject, ref } from 'vue';  /**  * 用于注入的 key  */ const PROVIDE_KEY = Symbol('oidc-provider'); /**  * 用户信息  */ interface UserInfo {   /**    * 用户 id    */   userId: string;   /**    * 用户名    */   username: string;   /**    * token    */   token: string;   /**    * 姓    */   lastName: string;   /**    * 名    */   firstName: string;   /**    * 邮箱    */   email: string;   /**    * 认证时间    */   authTime: number;   /**    * 角色    */   roles: Array<string>; } /**  * 认证管理器  */ class AuthManager {   /**    * token    */   accessToken = ref('');   /**    * 用户信息    */   userInfo = ref<UserInfo>();   /**    * oidc 客户端    */   private oidc: UserManager;   /**    * 构造函数    * @param settings oidc 客户端配置    */   constructor(settings: UserManagerSettings) {     this.oidc = new UserManager(settings);     // 当用户登录时,更新 token 和用户信息     this.oidc.events.addUserLoaded((user) => {       this.accessToken.value = user.access_token;       this.userInfo.value = {         userId: user.profile.sub,         username: user.profile.preferred_username || '',         token: user.access_token,         lastName: '',         firstName: '',         email: user.profile.email || '',         authTime: user.profile.auth_time || +new Date(),         roles: (user.profile.roles as Array<string>) || [],       };       // 开启静默刷新,清除过期状态       this.oidc.startSilentRenew();       this.oidc.clearStaleState();     });     // 当更新 token 失败时,退出登录     this.oidc.events.addSilentRenewError(() => {       this.logout();     });     // 当 token 过期时,退出登录     this.oidc.events.addAccessTokenExpired(() => {       this.logout();     });     // 初始化时加载用户信息     this.loadUser();   }   /**    * 加载用户信息    */   async loadUser() {     const user = await this.oidc.getUser();     // 如果能加载出来则将信息放到 Ref 里     if (user) {       this.accessToken.value = user.access_token;       this.userInfo.value = {         userId: user.profile.sub,         username: user.profile.preferred_username || '',         token: user.access_token,         lastName: '',         firstName: '',         email: user.profile.email || '',         authTime: user.profile.auth_time || +new Date(),         roles: (user.profile.roles as Array<string>) || [],       };       this.oidc.startSilentRenew();       this.oidc.clearStaleState();     }   }   /**    * 登录    */   login() {     return this.oidc.signinRedirect();   }   /**    * 检查是否已登录    * @returns 是否已登录    */   async checkLogin(): Promise<boolean> {     const user = await this.oidc.getUser();     return user != null && !user.expired;   }   /**    * 退出登录    */   logout() {     this.oidc.stopSilentRenew();     this.accessToken.value = '';     this.userInfo.value = undefined;     return this.oidc.signoutRedirect();   }   /**    * 刷新 token    * @param force 是否强制刷新    */   async refresh(force?: boolean) {     // 如果不是强制刷新,则先检查用户可用,如果用户可用则不刷新     if (!force) {       const user = await this.oidc.getUser();       if (user != null && !user.expired) {         return user;       }     }     return this.oidc.signinSilent();   }   /**    * 登录回调    */   loginCallback() {     return this.oidc.signinCallback();   }   /**    * 重置密码    */   resetPassword() {     // 这里使用 keycloak 登录流中的更新密码流实现     this.oidc.signinRedirect({       scope: 'openid',       extraQueryParams: {         // 这里设置额外参数时,带上 keycloak 的更新密码流         kc_action: 'UPDATE_PASSWORD',       },     });   } }  /**  * 认证插件  */ const authPlugin: Plugin<UserManagerSettings> = {   install: (app, options) => {     const auth = new AuthManager(options);     app.provide(PROVIDE_KEY, auth);   }, };  /**  * 使用认证管理器  * @returns 认证管理器  */ const useAuthManager = () => {   return inject<AuthManager>(PROVIDE_KEY); };  export { authPlugin, useAuthManager };