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

背景

目前单位系统常用 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

typescript 复制代码
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 };
相关推荐
昨晚我输给了一辆AE868 小时前
为什么现在不推荐使用 React.FC 了?
前端·react.js·typescript
Wect14 小时前
LeetCode 130. 被围绕的区域:两种解法详解(BFS/DFS)
前端·算法·typescript
Dilettante25815 小时前
这一招让 Node 后端服务启动速度提升 75%!
typescript·node.js
jonjia1 天前
模块、脚本与声明文件
typescript
jonjia1 天前
配置 TypeScript
typescript
jonjia1 天前
TypeScript 工具函数开发
typescript
jonjia1 天前
注解与断言
typescript
jonjia1 天前
IDE 超能力
typescript
jonjia1 天前
对象类型
typescript
jonjia1 天前
快速搭建 TypeScript 开发环境
typescript