- stores \ user
- query-keys.ts 統一管理Vue Query(TanStack Query的Vue適配版本)緩存鍵,在下面的文件中復用
- index.ts 入口文件,統一用戶信息查詢
- signed-in.ts 登錄狀態管理、認證邏輯
在用戶登錄后,系統頒發一個令牌,用于在后續的請求中證明“我是誰”
signed-in.js
const casdoorAuthRedirectPath = ‘/signed-in/callback’
定義認證回調路徑,用戶認證成功后重定向到此路徑
const casdoorSdk = new Sdk({
…casdoorConfig,
redirectPath: casdoorAuthRedirectPath
})
創建 Casdoor SDK 實例,展開配置并設置重定向路徑
const userStateStorageKey = ‘spx-user’
定義本地存儲鍵名,用于持久化用戶狀態
const userState = reactive({ // 返回Proxy代理對象,即所有屬性都變為響應式
accessToken: null as string | null, // 訪問令牌,用于API調用認證
// TS的類型注解語法,顯式聲明屬性的類型
初始化為 null ,然后后面只能改為類型為 string 或 null 類型的值
accessTokenExpiresAt: null as number | null, // 令牌過期時間戳:只能變成number類型
refreshToken: null as string | null // 刷新令牌,用于獲取新的訪問令牌
})
export function initUserState() {const stored = localStorage.getItem(userStateStorageKey)if (stored != null) {Object.assign(userState, JSON.parse(stored))}watchEffect(() => localStorage.setItem(userStateStorageKey, JSON.stringify(userState)))
}
持久化:從本地 localStorage 中恢復登錄狀態,從而減少沒必要的登錄請求和響應,提升用戶體驗,并在離線狀態下使用某些功能
并用 watchEffect 監聽狀態變化,自動同步到本地存儲
interface TokenResponse {access_token: stringexpires_in: numberrefresh_token: string
}
定義了令牌響應接口,描述了從認證服務器返回的令牌數據結構
(接口是一個契約,定義了對象有哪些對象以及分別是什么類型,而類定義了對象是如何創建、有哪些行為,提供了運行時邏輯)
function handleTokenResponse(resp: TokenResponse) {userState.accessToken = resp.access_tokenuserState.accessTokenExpiresAt = resp.expires_in ? Date.now() + resp.expires_in * 1000 : nulluserState.refreshToken = resp.refresh_token
}
令牌處理函數(令牌響應)
更新用戶的認證令牌
計算絕對過期時間:當前時間+有效期(記得乘以1000
更新用戶的刷新令牌
export function initiateSignIn(returnTo: string = window.location.pathname + window.location.search + window.location.hash
) {// Workaround for casdoor-js-sdk not supporting override of `redirectPath` in `signin_redirect`.const casdoorSdk = new Sdk({...casdoorConfig,redirectPath: `${casdoorAuthRedirectPath}?returnTo=${encodeURIComponent(returnTo)}`})casdoorSdk.signin_redirect()
}
發起登錄流程(returnTo屬性名稱:string類型 =賦值 當前頁面的路徑名+查詢參數+哈希值拼接結果,其中 window.location是瀏覽器環境的全局對象,表示當前頁面的 URL信息)
創建新的SDK實例
并通過 encodeURIComponent 對URI進行編碼防止XSS攻擊
通過新的SDK實例實現動態重定向
其中signin_redirect函數定義為
async signin_redirect(additionalParams) {window.location.assign(this.pkce.authorizeUrl(additionalParams));}
通過掛載在 window.location 上的 assign 方法將用戶的瀏覽器重定向到指定的 URL ,后面的 PKCE (Proof Key for Code Exchange) 授權是為了幫助客戶端安全地獲取訪問令牌
export async function completeSignIn() {const resp = await casdoorSdk.exchangeForAccessToken()handleTokenResponse(resp)
}
完成登錄流程,通過授權碼交換獲取訪問令牌(函數在sdk.js中定義了),處理返回的令牌數據
export function signInWithAccessToken(accessToken: string) {userState.accessToken = accessTokenuserState.accessTokenExpiresAt = nulluserState.refreshToken = null
}
- 使用現有訪問令牌直接登錄
- 清空過期時間和刷新令牌(假設外部令牌管理)
登出函數
export function signOut() {userState.accessToken = nulluserState.accessTokenExpiresAt = nulluserState.refreshToken = null
}
- 清空所有用戶狀態
- 實現完全登出
令牌刷新機制
const tokenExpiryDelta = 60 * 1000 // 1 minute in milliseconds
let tokenRefreshPromise: Promise<string | null> | null = null
- 定義令牌過期緩沖時間(1分鐘)
- 防止并發刷新的 Promise 變量
export async function ensureAccessToken(): Promise<string | null> {if (isAccessTokenValid()) return userState.accessTokenif (tokenRefreshPromise != null) return tokenRefreshPromiseif (userState.refreshToken == null) {signOut()return null}tokenRefreshPromise = (async () => {try {const resp = await casdoorSdk.refreshAccessToken(userState.refreshToken!)handleTokenResponse(resp)} catch (e) {console.error('failed to refresh access token', e)throw e}if (!isAccessTokenValid()) {signOut()return null}return userState.accessToken})()return tokenRefreshPromise.finally(() => (tokenRefreshPromise = null))
}
- 確保訪問令牌有效的核心函數
- 如果令牌有效,直接返回
- 防止并發刷新(通過 Promise 緩存)
- 無刷新令牌時直接登出
- 刷新失敗或新令牌無效時登出用戶
- 使用
finally
清理 Promise 緩存
狀態檢查函數
function isAccessTokenValid(): boolean {return !!(userState.accessToken &&(userState.accessTokenExpiresAt === null || userState.accessTokenExpiresAt - tokenExpiryDelta > Date.now()))
}
- 檢查訪問令牌是否有效
- 令牌存在且未過期(考慮緩沖時間)
- 使用雙重否定
!!
確保返回布爾值
export function isSignedIn(): boolean {return isAccessTokenValid() || userState.refreshToken != null
}
- 檢查用戶是否已登錄
- 有效訪問令牌或存在刷新令牌都視為已登錄
用戶信息獲取
export function getSignedInUsername(): string | null {if (!isSignedIn()) return nullif (!userState.accessToken) return nullconst decoded = jwtDecode<{ name: string }>(userState.accessToken)return decoded.name
}
- 從 JWT 令牌中解析用戶名
- 先檢查登錄狀態和令牌存在性
- 使用泛型指定 JWT 載荷類型
查詢相關功能
const signedInUserStaleTime = 60 * 1000 // 1minexport function getSignedInUserQueryKey() {return [...getUserQueryKey(getSignedInUsername() ?? ''), 'signed-in']
}
- 定義用戶信息緩存時間(1分鐘)
- 生成查詢鍵,包含用戶名和 'signed-in' 標識
export function useSignedInUser() {const queryKey = computed(() => getSignedInUserQueryKey())return useQueryWithCache({queryKey: queryKey,async queryFn() {if (!isSignedIn()) return nullreturn apis.getSignedInUser()},failureSummaryMessage: {en: 'Failed to load signed-in user information',zh: '加載當前用戶信息失敗'},staleTime: signedInUserStaleTime})
}
- Vue 組合式函數,用于獲取當前用戶信息
- 使用計算屬性動態生成查詢鍵
- 支持國際化錯誤消息
- 設置緩存失效時間
export function useUpdateSignedInUser() {const queryCache = useQueryCache()return useAction(async function updateSignedInUser(params: apis.UpdateSignedInUserParams) {const updated = await apis.updateSignedInUser(params)queryCache.invalidate(getUserQueryKey(getSignedInUsername()!))return updated},{ en: 'Failed to update profile', zh: '更新個人信息失敗' })
}
- Vue 組合式函數,用于更新用戶信息
- 更新成功后使查詢緩存失效,觸發重新獲取
- 使用非空斷言操作符
!
確保用戶名存在 - 支持國際化錯誤消息
總結
這個文件實現了一個完整的用戶認證系統,包括:
- 狀態管理: 使用 Vue 3 響應式系統管理用戶狀態
- 持久化: 自動同步狀態到本地存儲
- 認證流程: 支持 OAuth 2.0 授權碼流程
- 令牌管理: 自動刷新訪問令牌,防止并發刷新
- 錯誤處理: 完善的錯誤處理和用戶登出機制
- 緩存集成: 與查詢緩存系統集成,優化數據獲取
- 國際化: 支持中英文錯誤消息
代碼設計良好,考慮了安全性、性能和用戶體驗等多個方面。