背景 目前單位系統常用 Keycloak 作為認證系統後端,而前端之前寫的也比較隨意,這次用 Vue 3 插件以及 Ref 響應式來編寫這個模塊。另外,這個可能是全網唯一使用 keycloak 的 OIDC 原生更新密碼流的介紹代碼。 設計 依賴庫選擇 OIDC 客戶端,這裡選擇 oidc-clie ...
背景
目前單位系統常用 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 };