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:
232
frontend/app/web-gold/src/services/AuthService.js
Normal file
232
frontend/app/web-gold/src/services/AuthService.js
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 认证服务 - 封装所有认证相关业务逻辑
|
||||
* 提供登录、登出、刷新token等方法
|
||||
* 函数式实现,无 this 绑定问题
|
||||
*/
|
||||
|
||||
import { clientAxios } from '@gold/api/axios/client'
|
||||
import tokenManager from '@gold/utils/token-manager'
|
||||
|
||||
// 刷新token的锁,避免并发刷新
|
||||
let isRefreshing = false
|
||||
let refreshPromise = null
|
||||
|
||||
/**
|
||||
* 手机号+验证码登录
|
||||
*/
|
||||
async function loginBySms(mobile, code) {
|
||||
try {
|
||||
const { data } = await clientAxios.post('/api/member/auth/sms-login', { mobile, code })
|
||||
const tokenInfo = data || {}
|
||||
|
||||
// 保存token
|
||||
tokenManager.setTokens({
|
||||
accessToken: tokenInfo.accessToken || '',
|
||||
refreshToken: tokenInfo.refreshToken || '',
|
||||
expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200,
|
||||
tokenType: 'Bearer'
|
||||
})
|
||||
|
||||
return tokenInfo
|
||||
} catch (error) {
|
||||
console.error('短信登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号密码登录
|
||||
*/
|
||||
async function loginByPassword(mobile, password) {
|
||||
try {
|
||||
const { data } = await clientAxios.post('/api/member/auth/login', { mobile, password })
|
||||
const tokenInfo = data || {}
|
||||
|
||||
tokenManager.setTokens({
|
||||
accessToken: tokenInfo.accessToken || '',
|
||||
refreshToken: tokenInfo.refreshToken || '',
|
||||
expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200,
|
||||
tokenType: 'Bearer'
|
||||
})
|
||||
|
||||
return tokenInfo
|
||||
} catch (error) {
|
||||
console.error('密码登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行实际刷新token的逻辑
|
||||
*/
|
||||
async function doRefreshToken(refreshToken) {
|
||||
const { data } = await clientAxios.post('/api/member/auth/refresh-token', null, {
|
||||
params: { refreshToken }
|
||||
})
|
||||
const tokenInfo = data || {}
|
||||
|
||||
tokenManager.setTokens({
|
||||
accessToken: tokenInfo.accessToken || '',
|
||||
refreshToken: tokenInfo.refreshToken || '',
|
||||
expiresIn: tokenInfo.expiresTime ? Math.floor(tokenInfo.expiresTime / 1000) : 7200,
|
||||
tokenType: 'Bearer'
|
||||
})
|
||||
|
||||
return tokenInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* 使用锁机制防止并发刷新
|
||||
*/
|
||||
async function refreshToken() {
|
||||
// 如果正在刷新,返回已有的刷新Promise
|
||||
if (isRefreshing && refreshPromise) {
|
||||
return refreshPromise
|
||||
}
|
||||
|
||||
const refreshTokenValue = tokenManager.getRefreshToken()
|
||||
if (!refreshTokenValue) {
|
||||
throw new Error('缺少refreshToken,无法刷新')
|
||||
}
|
||||
|
||||
try {
|
||||
isRefreshing = true
|
||||
refreshPromise = doRefreshToken(refreshTokenValue)
|
||||
const result = await refreshPromise
|
||||
return result
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
refreshPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
async function sendSmsCode(mobile, scene = 1) {
|
||||
const { data } = await clientAxios.post('/api/member/auth/send-sms-code', {
|
||||
mobile,
|
||||
scene
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
async function logout() {
|
||||
try {
|
||||
// 清除token
|
||||
tokenManager.clearTokens()
|
||||
|
||||
// 跳转到登录页
|
||||
window.location.href = '/login'
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error)
|
||||
// 即使出错也要清除token
|
||||
tokenManager.clearTokens()
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
*/
|
||||
function isAuthenticated() {
|
||||
return tokenManager.isLoggedIn()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
async function getCurrentUser() {
|
||||
if (!isAuthenticated()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await clientAxios.get('/api/member/user/get')
|
||||
return data || null
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理401/403错误
|
||||
* @param {Error} error - axios错误对象
|
||||
* @param {Function} onAuthFailed - 认证失败回调
|
||||
* @param {Object} options - 额外选项
|
||||
* @param {AxiosInstance} options.httpClient - 可选的HTTP客户端实例,用于重新发起请求
|
||||
*/
|
||||
async function handleAuthError(error, onAuthFailed, options = {}) {
|
||||
const status = error?.response?.status
|
||||
const data = error?.response?.data
|
||||
const code = data?.code
|
||||
const originalRequest = error.config
|
||||
const { httpClient } = options
|
||||
|
||||
// 统一检查是否为 401 (token无效或过期)
|
||||
const is401 = (code === 401) || (status === 401)
|
||||
|
||||
// 统一检查是否为 403 (没有权限)
|
||||
const is403 = (code === 403) || (status === 403)
|
||||
|
||||
// 处理 401 错误:尝试刷新 token
|
||||
if (is401) {
|
||||
try {
|
||||
// 刷新 token
|
||||
await refreshToken()
|
||||
|
||||
// 刷新成功,重新发起原请求(如果提供了 httpClient)
|
||||
if (originalRequest && httpClient) {
|
||||
originalRequest.headers['Authorization'] = tokenManager.getAuthHeader()
|
||||
return httpClient(originalRequest)
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (refreshError) {
|
||||
console.error('刷新token失败:', refreshError)
|
||||
if (typeof onAuthFailed === 'function') {
|
||||
onAuthFailed()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 403 错误:直接调用回调
|
||||
if (is403) {
|
||||
if (typeof onAuthFailed === 'function') {
|
||||
onAuthFailed()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// 其他错误直接抛出
|
||||
throw error
|
||||
}
|
||||
|
||||
export default {
|
||||
loginBySms,
|
||||
loginByPassword,
|
||||
refreshToken,
|
||||
sendSmsCode,
|
||||
logout,
|
||||
isAuthenticated,
|
||||
getCurrentUser,
|
||||
handleAuthError
|
||||
}
|
||||
|
||||
// 导出便捷方法(解构赋值,不会有 this 问题)
|
||||
export {
|
||||
loginBySms,
|
||||
loginByPassword,
|
||||
refreshToken,
|
||||
sendSmsCode,
|
||||
logout,
|
||||
isAuthenticated,
|
||||
getCurrentUser,
|
||||
handleAuthError
|
||||
}
|
||||
Reference in New Issue
Block a user