• 使用 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

    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 };
    
    
  • 相关阅读:
    js将html网页转换成PDF并解决了图表文字被切割的问题
    NetCore框架WTM的分表分库实现
    【网络协议】聊聊HTTPDNS如何工作的
    C++ 四种类型转换
    死磕它七年“腾讯限量版”Java架构笔记,要个40k不过分吧?
    【ArcGIS Pro二次开发】(83):ProWindow和WPF的一些技巧
    网络问题排障专题-AF网络问题排障
    如何获取淘宝sku详细信息 API接口
    文件上传复习(upload-labs14-17关)
    SpringCloud Eureka搭建会员中心服务提供方-集群
  • 原文地址:https://www.cnblogs.com/aobaxu/p/17810124.html