/** * Token 统一管理器 * * 功能特性: * - ✅ 安全存储访问令牌、刷新令牌、过期时间等信息 * - ✅ 自动检查令牌是否过期(支持提前刷新缓冲时间) * * ✅ 提供订阅者模式,监听令牌变化事件 * ✅ 兼容旧版本 API,提供 getToken()、removeToken() 等方法 * * 使用示例: * ```javascript * import { getAccessToken, setTokens, isLoggedIn } from '@gold/utils/token-manager' * * // 检查登录状态 * if (isLoggedIn()) { * const token = getAccessToken() * } * * // 设置令牌 * setTokens({ * accessToken: 'eyJ...', * refreshToken: 'eyJ...', * expiresIn: 7200, * tokenType: 'Bearer' * }) * ``` */ // localStorage 中存储的键名常量 const TOKEN_KEYS = { ACCESS_TOKEN: 'access_token', REFRESH_TOKEN: 'refresh_token', EXPIRES_TIME: 'expires_time', TOKEN_TYPE: 'token_type', } class TokenManager { constructor() { this.subscribers = [] // 订阅token变化的回调 } /** * 解析 LocalDateTime 格式为毫秒时间戳(使用 dayjs) * @param {string} dateTimeStr - LocalDateTime 格式字符串,如 "2025-12-27T10:27:42" * @returns {number} Unix 时间戳(毫秒) */ parseLocalDateTime(dateTimeStr) { if (!dateTimeStr) return 0 // 使用 dayjs 解析 LocalDateTime 格式 const normalizedStr = dateTimeStr.includes(' ') ? dateTimeStr.replace(' ', 'T') : dateTimeStr const dayjs = require('dayjs') const parsedTime = dayjs(normalizedStr) if (!parsedTime.isValid()) { console.warn('[TokenManager] 无法解析过期时间:', dateTimeStr) return 0 } return parsedTime.valueOf() // 返回毫秒时间戳 } /** * 获取访问令牌 * @returns {string|null} 访问令牌,如果不存在则返回 null */ getAccessToken() { return localStorage.getItem(TOKEN_KEYS.ACCESS_TOKEN) } /** * 获取刷新令牌 * @returns {string|null} 刷新令牌,如果不存在则返回 null */ getRefreshToken() { return localStorage.getItem(TOKEN_KEYS.REFRESH_TOKEN) } /** * 获取过期时间(毫秒时间戳) * @returns {number} 过期时间戳,如果未设置则返回 0 */ getExpiresTime() { const expiresTimeStr = localStorage.getItem(TOKEN_KEYS.EXPIRES_TIME) return expiresTimeStr ? parseInt(expiresTimeStr, 10) : 0 } /** * 获取令牌类型 * @returns {string} 令牌类型,默认为 'Bearer' */ getTokenType() { return localStorage.getItem(TOKEN_KEYS.TOKEN_TYPE) || 'Bearer' } /** * 检查访问令牌是否已过期 * @param {number} bufferTime - 提前刷新缓冲时间(毫秒),默认 5 分钟 * @returns {boolean} 如果已过期或即将过期(超过缓冲时间)则返回 true */ isExpired(bufferTime = 5 * 60 * 1000) { const expiresTime = this.getExpiresTime() const now = Date.now() // 没有过期时间或当前时间已超过(过期时间 - 缓冲时间) return !expiresTime || now >= (expiresTime - bufferTime) } /** * 检查用户是否已登录(有有效的访问令牌) * @returns {boolean} 如果有有效令牌则返回 true */ isLoggedIn() { const token = this.getAccessToken() return Boolean(token) && !this.isExpired() } /** * 存储访问令牌和相关信息到本地存储 * @param {Object} tokenInfo - 令牌信息对象 * @param {string} tokenInfo.accessToken - 访问令牌(必填) * @param {string} tokenInfo.refreshToken - 刷新令牌(可选) * @param {number} tokenInfo.expiresIn - 令牌有效期(秒,可选) * @param {string|number} tokenInfo.expiresTime - 过期时间(可选,支持 LocalDateTime 字符串、数字格式) * @param {string} tokenInfo.tokenType - 令牌类型,默认为 'Bearer' */ setTokens(tokenInfo) { const { accessToken, refreshToken, expiresIn, expiresTime, tokenType = 'Bearer' } = tokenInfo // 校验:必须提供访问令牌 if (!accessToken) { console.error('[TokenManager] 设置令牌失败:缺少 accessToken') return } // 存储到 localStorage localStorage.setItem(TOKEN_KEYS.ACCESS_TOKEN, accessToken) if (refreshToken) { localStorage.setItem(TOKEN_KEYS.REFRESH_TOKEN, refreshToken) } // 处理过期时间 let expiresTimeMs = 0 if (expiresTime) { // 检查类型并转换 if (typeof expiresTime === 'string' && expiresTime.includes('T')) { // LocalDateTime 格式 expiresTimeMs = this.parseLocalDateTime(expiresTime) } else if (typeof expiresTime === 'number') { // 数字格式(可能是秒或毫秒) expiresTimeMs = expiresTime > 10000000000 ? expiresTime : expiresTime * 1000 } else if (expiresIn) { // 通过 expiresIn 计算 expiresTimeMs = Date.now() + (expiresIn * 1000) } if (expiresTimeMs) { localStorage.setItem(TOKEN_KEYS.EXPIRES_TIME, String(expiresTimeMs)) } } localStorage.setItem(TOKEN_KEYS.TOKEN_TYPE, tokenType) // 通知所有订阅者令牌已更新 this.notifySubscribers('token-set', tokenInfo) } /** * 清除所有存储的令牌信息 * 从 localStorage 中删除所有令牌相关数据,并通知订阅者 */ clearTokens() { // 删除所有令牌相关的 localStorage 项 Object.values(TOKEN_KEYS).forEach(key => { localStorage.removeItem(key) }) // 通知所有订阅者令牌已被清除 this.notifySubscribers('token-cleared', null) } /** * 生成完整的 Authorization 请求头值 * @returns {string} 例如:"Bearer eyJhbGciOi...",如果没有令牌则返回空字符串 */ getAuthHeader() { const token = this.getAccessToken() const tokenType = this.getTokenType() // 只有当令牌存在时才返回完整的 Authorization 头 return token ? `${tokenType} ${token}` : '' } /** * 订阅令牌变化事件 * @param {Function} callback - 回调函数,接收 (type, tokenInfo) 两个参数 * - type: 事件类型('token-set' | 'token-cleared') * - tokenInfo: 令牌信息对象或 null */ subscribe(callback) { // 仅接受函数类型的回调 if (typeof callback === 'function') { this.subscribers.push(callback) } } /** * 取消令牌变化事件订阅 * @param {Function} callback - 要取消的回调函数 */ unsubscribe(callback) { const index = this.subscribers.indexOf(callback) // 如果找到回调函数,则从数组中移除 if (index !== -1) { this.subscribers.splice(index, 1) } } /** * 通知所有订阅者令牌已发生变化 * @param {string} type - 事件类型 * @param {Object|null} tokenInfo - 令牌信息 */ notifySubscribers(type, tokenInfo) { // 遍历所有订阅者并调用其回调函数 this.subscribers.forEach(callback => { try { callback(type, tokenInfo) } catch (error) { // 订阅者回调出错不应影响其他回调的执行 console.error('[TokenManager] 订阅者回调执行失败:', error) } }) } /** * 【兼容方法】获取访问令牌 * @deprecated 请使用 getAccessToken() 替代 * @returns {string|null} 访问令牌 */ getToken() { return this.getAccessToken() } /** * 【兼容方法】移除所有令牌 * @deprecated 请使用 clearTokens() 替代 */ removeToken() { this.clearTokens() } } // 创建 TokenManager 单例实例 const tokenManager = new TokenManager() // 最简单的导出方式:直接导出实例 // 使用方法:import tokenManager from '@gold/utils/token-manager' // tokenManager.getAccessToken() export default tokenManager