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:
2025-11-25 00:58:51 +08:00
parent cea43dd635
commit fb6d18b4f5
24 changed files with 1148 additions and 1198 deletions

View File

@@ -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 } });

View File

@@ -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 // 禁用自动重试

View File

@@ -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

View File

@@ -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 实例重新发起请求,
* 确保自定义拦截器被正确应用。
*/