- A+
所属分类:Web前端
背景
目前单位系统常用 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 };