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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
|
||||
import api from '@/api/http'
|
||||
import { setToken, getRefreshToken } from '@/utils/auth'
|
||||
// 直接使用实例(最简单、最可靠)
|
||||
import tokenManager from '@gold/utils/token-manager'
|
||||
// 使用公共配置
|
||||
import { API_BASE } from '@gold/config/api'
|
||||
|
||||
@@ -12,7 +13,7 @@ const SERVER_BASE = API_BASE.APP_MEMBER
|
||||
*/
|
||||
function saveTokens(info) {
|
||||
if (info?.accessToken || info?.refreshToken) {
|
||||
setToken({
|
||||
tokenManager.setTokens({
|
||||
accessToken: info.accessToken || '',
|
||||
refreshToken: info.refreshToken || '',
|
||||
})
|
||||
@@ -118,7 +119,7 @@ export async function loginBySms(mobile, code) {
|
||||
* @returns {Promise<Object>} data.data: { accessToken, refreshToken, expiresTime, userInfo }
|
||||
*/
|
||||
export async function refreshToken() {
|
||||
const rt = getRefreshToken();
|
||||
const rt = tokenManager.getRefreshToken();
|
||||
if (!rt) throw new Error('缺少 refresh_token');
|
||||
// refreshToken 作为 URL 查询参数传递
|
||||
const { data } = await api.post(`${SERVER_BASE}/auth/refresh-token`, null, { params: { refreshToken: rt } });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import request from '@/api/http'
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
// 直接使用 tokenManager 实例(最简单、最可靠)
|
||||
import tokenManager from '@gold/utils/token-manager'
|
||||
// 使用公共配置
|
||||
import { API_BASE } from '@gold/config/api'
|
||||
|
||||
@@ -32,7 +33,7 @@ export const ChatMessageApi = {
|
||||
onClose,
|
||||
attachmentUrls = []
|
||||
} = options || {}
|
||||
const token = getAccessToken()
|
||||
const token = tokenManager.getAccessToken()
|
||||
|
||||
let retryCount = 0
|
||||
const maxRetries = 0 // 禁用自动重试
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import { getAuthHeader } from '@gold/utils/token-manager'
|
||||
// 直接使用实例(最简单、最可靠)
|
||||
import tokenManager from '@gold/utils/token-manager'
|
||||
import { TikHubService } from '@gold/api/services'
|
||||
import { API_BASE } from '@gold/config/api'
|
||||
|
||||
@@ -43,9 +44,9 @@ export const CommonService = {
|
||||
onError,
|
||||
onClose
|
||||
} = options || {}
|
||||
|
||||
const authHeader = getAuthHeader()
|
||||
|
||||
|
||||
const authHeader = tokenManager.getAuthHeader()
|
||||
|
||||
let retryCount = 0
|
||||
const maxRetries = 0
|
||||
|
||||
|
||||
@@ -1,168 +1,110 @@
|
||||
/**
|
||||
* 应用层 HTTP 客户端
|
||||
* 使用 mono 级别的 axios 实例,添加应用特定的 401 处理
|
||||
* 支持自定义拦截器的可扩展版本
|
||||
* - 提供 createHttpClient 工厂函数
|
||||
* - 业务代码可以创建自己的 http 实例并添加拦截器
|
||||
*/
|
||||
|
||||
import { message } from 'ant-design-vue'
|
||||
import { clearAllTokens, getRefreshToken } from '@gold/utils/token-manager'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { createClientAxios } from '@gold/api/axios/client'
|
||||
import { refreshToken } from '@/api/auth'
|
||||
|
||||
// 刷新 token 的状态管理
|
||||
let isRefreshing = false
|
||||
let refreshPromise = null
|
||||
import authService from '@/services/AuthService'
|
||||
|
||||
/**
|
||||
* 处理 403 禁止访问错误(应用层特定逻辑)
|
||||
* 先尝试刷新 token,如果失败或没有 refresh token 才提示用户
|
||||
* 创建应用层 HTTP 客户端实例
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {Function} options.on401 - 401 错误处理回调
|
||||
* @param {Function} options.on403 - 403 错误处理回调
|
||||
* @returns {AxiosInstance} HTTP 客户端实例
|
||||
*/
|
||||
async function handleApp403Error() {
|
||||
// 避免重复处理
|
||||
if (handleApp403Error.processed) {
|
||||
return
|
||||
}
|
||||
|
||||
handleApp403Error.processed = true
|
||||
|
||||
try {
|
||||
// 检查是否有 refresh token
|
||||
const refreshTokenValue = getRefreshToken()
|
||||
|
||||
if (refreshTokenValue) {
|
||||
// 如果有 refresh token,尝试刷新
|
||||
try {
|
||||
// 如果正在刷新,等待刷新完成
|
||||
if (isRefreshing && refreshPromise) {
|
||||
await refreshPromise
|
||||
handleApp403Error.processed = false
|
||||
return
|
||||
}
|
||||
|
||||
// 开始刷新 token
|
||||
isRefreshing = true
|
||||
refreshPromise = refreshToken()
|
||||
|
||||
await refreshPromise
|
||||
|
||||
// 刷新成功,重置状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
handleApp403Error.processed = false
|
||||
|
||||
// 刷新成功,不提示用户(静默刷新)
|
||||
return
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,清除状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
console.error('刷新 token 失败:', refreshError)
|
||||
|
||||
// 刷新失败才提示用户
|
||||
message.warning('登录状态已过期,请重新登录', 3)
|
||||
handleApp403Error.processed = false
|
||||
return
|
||||
export function createHttpClient(options = {}) {
|
||||
const { on401, on403 } = options
|
||||
|
||||
const httpClient = createClientAxios({
|
||||
baseURL: '/',
|
||||
timeout: 180000,
|
||||
on401: (error) => {
|
||||
if (on401) {
|
||||
on401(error)
|
||||
} else {
|
||||
// 默认使用 AuthService 处理,并传入 httpClient 确保拦截器生效
|
||||
authService.handleAuthError(
|
||||
error,
|
||||
() => window.location.href = '/login',
|
||||
{ httpClient } // 关键:传入 HTTP 实例
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 没有 refresh token,提示用户
|
||||
message.warning('登录状态已过期,请重新登录', 3)
|
||||
handleApp403Error.processed = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理 403 错误失败:', e)
|
||||
handleApp403Error.processed = false
|
||||
}
|
||||
},
|
||||
on403: (error) => {
|
||||
if (on403) {
|
||||
on403(error)
|
||||
} else {
|
||||
// 默认使用 AuthService 处理,并传入 httpClient 确保拦截器生效
|
||||
authService.handleAuthError(
|
||||
error,
|
||||
() => window.location.href = '/login',
|
||||
{ httpClient } // 关键:传入 HTTP 实例
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return httpClient
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 401 未授权错误(应用层特定逻辑)
|
||||
* 先尝试刷新 token,如果失败或没有 refresh token 才退出登录
|
||||
* 默认的 HTTP 客户端实例
|
||||
* 业务代码可以:
|
||||
* 1. 直接使用 http 实例
|
||||
* 2. 调用 createHttpClient() 创建自定义实例
|
||||
* 3. 在自定义实例上添加拦截器
|
||||
*/
|
||||
async function handleApp401Error() {
|
||||
// 避免重复处理
|
||||
if (handleApp401Error.processed) {
|
||||
return
|
||||
}
|
||||
|
||||
handleApp401Error.processed = true
|
||||
|
||||
try {
|
||||
// 检查是否有 refresh token
|
||||
const refreshTokenValue = getRefreshToken()
|
||||
|
||||
if (refreshTokenValue) {
|
||||
// 如果有 refresh token,尝试刷新
|
||||
try {
|
||||
// 如果正在刷新,等待刷新完成
|
||||
if (isRefreshing && refreshPromise) {
|
||||
await refreshPromise
|
||||
handleApp401Error.processed = false
|
||||
return
|
||||
}
|
||||
|
||||
// 开始刷新 token
|
||||
isRefreshing = true
|
||||
refreshPromise = refreshToken()
|
||||
|
||||
await refreshPromise
|
||||
|
||||
// 刷新成功,重置状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
handleApp401Error.processed = false
|
||||
|
||||
// 刷新成功,不提示用户(静默刷新)
|
||||
return
|
||||
} catch (refreshError) {
|
||||
// 刷新失败,清除状态
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
console.error('刷新 token 失败:', refreshError)
|
||||
|
||||
// 继续执行退出登录逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 没有 refresh token 或刷新失败,执行退出登录
|
||||
try {
|
||||
clearAllTokens()
|
||||
} catch (e) {
|
||||
console.error('清空 token 失败:', e)
|
||||
}
|
||||
|
||||
try {
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
} catch (e) {
|
||||
console.error('退出登录失败:', e)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
message.warning('登录已过期,请重新登录', 3)
|
||||
}, 100)
|
||||
|
||||
} catch (e) {
|
||||
console.error('处理 401 错误失败:', e)
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
handleApp401Error.processed = false
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应用层 HTTP 客户端
|
||||
* 基于 mono 级别的 axios 实例,添加应用特定的错误处理
|
||||
*/
|
||||
const http = createClientAxios({
|
||||
baseURL: '/',
|
||||
timeout: 180000,
|
||||
on401: handleApp401Error,
|
||||
on403: handleApp403Error,
|
||||
})
|
||||
|
||||
// 注意:403 处理已在 createClientAxios 的响应拦截器中通过 on403 回调处理
|
||||
const http = createHttpClient()
|
||||
|
||||
export default http
|
||||
|
||||
/**
|
||||
* 使用示例:
|
||||
*
|
||||
* 1. 直接使用默认实例
|
||||
* import http from '@/api/http'
|
||||
* await http.post('/api/data')
|
||||
*
|
||||
* 2. 创建自定义实例并添加拦截器
|
||||
* import { createHttpClient } from '@/api/http'
|
||||
*
|
||||
* const myHttp = createHttpClient()
|
||||
* myHttp.interceptors.request.use((config) => {
|
||||
* // 添加自定义请求头
|
||||
* config.headers['X-Custom-Header'] = 'value'
|
||||
* return config
|
||||
* })
|
||||
* await myHttp.post('/api/data')
|
||||
*
|
||||
* 3. 自定义 401 处理
|
||||
* import { createHttpClient } from '@/api/http'
|
||||
*
|
||||
* const myHttp = createHttpClient({
|
||||
* on401: (error) => {
|
||||
* // 自定义 401 处理逻辑
|
||||
* console.log('自定义 401 处理')
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* 4. 高级用法:自动刷新 token 并重试请求
|
||||
* import { createHttpClient } from '@/api/http'
|
||||
*
|
||||
* const myHttp = createHttpClient()
|
||||
*
|
||||
* try {
|
||||
* const response = await myHttp.post('/api/data')
|
||||
* console.log(response.data)
|
||||
* } catch (error) {
|
||||
* // 如果是 401 且刷新失败,会自动调用 onAuthFailed 回调
|
||||
* console.error('请求失败:', error.message)
|
||||
* }
|
||||
*
|
||||
* 注意:当 401 发生时,AuthService 会自动尝试刷新 token。
|
||||
* 如果刷新成功,会使用相同的 http 实例重新发起请求,
|
||||
* 确保自定义拦截器被正确应用。
|
||||
*/
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user