核心改进: - 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>
233 lines
6.4 KiB
JavaScript
233 lines
6.4 KiB
JavaScript
/**
|
||
* 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
|