- 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 授权码流程
- 令牌管理: 自动刷新访问令牌,防止并发刷新
- 错误处理: 完善的错误处理和用户登出机制
- 缓存集成: 与查询缓存系统集成,优化数据获取
- 国际化: 支持中英文错误消息
代码设计良好,考虑了安全性、性能和用户体验等多个方面。