Files
sionrui/frontend/utils/token-manager.js

255 lines
7.2 KiB
JavaScript
Raw Normal View History

2025-11-12 22:45:29 +08:00
/**
* Token 统一管理器
*
* 功能特性
* - 安全存储访问令牌刷新令牌过期时间等信息
* - 自动检查令牌是否过期支持提前刷新缓冲时间
* * 提供订阅者模式监听令牌变化事件
*
2026-01-17 14:43:42 +08:00
2025-11-12 22:45:29 +08:00
*/
// localStorage 中存储的键名常量
2025-11-12 22:45:29 +08:00
const TOKEN_KEYS = {
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
EXPIRES_TIME: 'expires_time',
TOKEN_TYPE: 'token_type',
2025-11-12 22:45:29 +08:00
}
class TokenManager {
constructor() {
this.subscribers = [] // 订阅token变化的回调
2025-11-12 22:45:29 +08:00
}
/**
2026-02-08 19:46:17 +08:00
* 解析 LocalDateTime 格式为毫秒时间戳
* @param {string} dateTimeStr - LocalDateTime 格式字符串 "2025-12-27T10:27:42"
* @returns {number} Unix 时间戳毫秒
*/
parseLocalDateTime(dateTimeStr) {
if (!dateTimeStr) return 0
2026-02-08 19:46:17 +08:00
// 规范化日期字符串:将空格替换为 T
const normalizedStr = dateTimeStr.includes(' ')
? dateTimeStr.replace(' ', 'T')
: dateTimeStr
2026-02-08 19:46:17 +08:00
// 使用原生 Date API 解析
const parsedTime = new Date(normalizedStr)
2026-02-08 19:46:17 +08:00
if (isNaN(parsedTime.getTime())) {
console.warn('[TokenManager] 无法解析过期时间:', dateTimeStr)
return 0
}
2026-02-08 19:46:17 +08:00
return parsedTime.getTime() // 返回毫秒时间戳
}
/**
* 获取访问令牌
* @returns {string|null} 访问令牌如果不存在则返回 null
*/
getAccessToken() {
return localStorage.getItem(TOKEN_KEYS.ACCESS_TOKEN)
}
2025-11-12 22:45:29 +08:00
/**
* 获取刷新令牌
* @returns {string|null} 刷新令牌如果不存在则返回 null
*/
getRefreshToken() {
return localStorage.getItem(TOKEN_KEYS.REFRESH_TOKEN)
2025-11-12 22:45:29 +08:00
}
/**
* 获取过期时间毫秒时间戳
* @returns {number} 过期时间戳如果未设置则返回 0
*/
getExpiresTime() {
const expiresTimeStr = localStorage.getItem(TOKEN_KEYS.EXPIRES_TIME)
return expiresTimeStr ? parseInt(expiresTimeStr, 10) : 0
2025-11-12 22:45:29 +08:00
}
/**
* 获取令牌类型
* @returns {string} 令牌类型默认为 'Bearer'
*/
getTokenType() {
return localStorage.getItem(TOKEN_KEYS.TOKEN_TYPE) || 'Bearer'
2025-11-12 22:45:29 +08:00
}
/**
* 检查访问令牌是否已过期
* @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)
}
2025-11-12 22:45:29 +08:00
/**
* 检查用户是否已登录有有效的访问令牌
* @returns {boolean} 如果有有效令牌则返回 true
*/
isLoggedIn() {
const token = this.getAccessToken()
return Boolean(token) && !this.isExpired()
2025-11-12 22:45:29 +08:00
}
/**
* 存储访问令牌和相关信息到本地存储
* @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
}
2025-11-12 22:45:29 +08:00
// 存储到 localStorage
localStorage.setItem(TOKEN_KEYS.ACCESS_TOKEN, accessToken)
if (refreshToken) {
localStorage.setItem(TOKEN_KEYS.REFRESH_TOKEN, refreshToken)
2025-11-12 22:45:29 +08:00
}
// 处理过期时间
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))
}
2025-11-12 22:45:29 +08:00
}
localStorage.setItem(TOKEN_KEYS.TOKEN_TYPE, tokenType)
// 通知所有订阅者令牌已更新
this.notifySubscribers('token-set', tokenInfo)
2025-11-12 22:45:29 +08:00
}
/**
* 清除所有存储的令牌信息
* localStorage 中删除所有令牌相关数据并通知订阅者
*/
clearTokens() {
// 删除所有令牌相关的 localStorage 项
Object.values(TOKEN_KEYS).forEach(key => {
localStorage.removeItem(key)
})
// 通知所有订阅者令牌已被清除
this.notifySubscribers('token-cleared', null)
2025-11-12 22:45:29 +08:00
}
/**
* 生成完整的 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)
}
2025-11-12 22:45:29 +08:00
}
/**
* 取消令牌变化事件订阅
* @param {Function} callback - 要取消的回调函数
*/
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback)
// 如果找到回调函数,则从数组中移除
if (index !== -1) {
this.subscribers.splice(index, 1)
}
2025-11-12 22:45:29 +08:00
}
/**
* 通知所有订阅者令牌已发生变化
* @param {string} type - 事件类型
* @param {Object|null} tokenInfo - 令牌信息
*/
notifySubscribers(type, tokenInfo) {
// 遍历所有订阅者并调用其回调函数
this.subscribers.forEach(callback => {
2025-11-12 22:45:29 +08:00
try {
callback(type, tokenInfo)
} catch (error) {
// 订阅者回调出错不应影响其他回调的执行
console.error('[TokenManager] 订阅者回调执行失败:', error)
2025-11-12 22:45:29 +08:00
}
})
}
/**
* 兼容方法获取访问令牌
* @deprecated 请使用 getAccessToken() 替代
* @returns {string|null} 访问令牌
*/
getToken() {
return this.getAccessToken()
}
2025-11-12 22:45:29 +08:00
/**
* 兼容方法移除所有令牌
* @deprecated 请使用 clearTokens() 替代
*/
removeToken() {
this.clearTokens()
}
2025-11-12 22:45:29 +08:00
}
// 创建 TokenManager 单例实例
const tokenManager = new TokenManager()
// 最简单的导出方式:直接导出实例
// 使用方法import tokenManager from '@gold/utils/token-manager'
// tokenManager.getAccessToken()
export default tokenManager