Files
sionrui/frontend/utils/token-manager.js
sion123 fb6d18b4f5 feat: 重构HTTP客户端架构和认证系统
核心改进:
- HTTP客户端:工厂函数模式,支持自定义拦截器和401/403处理
- 认证服务:函数式实现,消除this绑定问题,支持业务码+HTTP状态码双通道
- Token管理:简化为直接实例导出,移除bind()和箭头函数包装
- 路由守卫:优化逻辑,移除冗余代码,更简洁易维护

技术亮点:
- 统一401/403错误处理(业务code和HTTP status双检查)
- 自动刷新token并重试请求,保留自定义拦截器
- 分层清晰:clientAxios (Mono) -> http (应用) -> AuthService
- 支持扩展:业务代码可创建自定义HTTP实例并添加拦截器

文件变更:
- 新增 AuthService.js (函数式) 和 Login.vue
- 重构 http.js、token-manager.js、router/index.js
- 删除 TokenInput.vue、utils/auth.js 等冗余文件
- 更新所有API调用点使用直接实例导入

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 00:58:51 +08:00

233 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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变化的回调
}
/**
* 获取访问令牌
* @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} tokenInfo.tokenType - 令牌类型,默认为 'Bearer'
*/
setTokens(tokenInfo) {
const {
accessToken,
refreshToken,
expiresIn,
tokenType = 'Bearer'
} = tokenInfo
// 校验:必须提供访问令牌
if (!accessToken) {
console.error('[TokenManager] 设置令牌失败:缺少 accessToken')
return
}
// 将过期时间从秒转换为毫秒时间戳
const expiresTime = expiresIn ? Date.now() + (expiresIn * 1000) : 0
// 存储到 localStorage
localStorage.setItem(TOKEN_KEYS.ACCESS_TOKEN, accessToken)
if (refreshToken) {
localStorage.setItem(TOKEN_KEYS.REFRESH_TOKEN, refreshToken)
}
if (expiresTime) {
localStorage.setItem(TOKEN_KEYS.EXPIRES_TIME, String(expiresTime))
}
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