feat(auth): 实现用户登出功能并增强错误处理
添加用户登出方法到用户store,替换多处手动更新登录状态的代码 新增错误码枚举和映射,完善axios拦截器中的错误处理逻辑 重构token刷新逻辑为独立函数,支持401错误自动刷新token
This commit is contained in:
@@ -131,10 +131,7 @@ const goToSettings = () => {
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
clearTokens();
|
||||
userStore.updateUserLoginStatus({
|
||||
userName: "未登录",
|
||||
userRole: ACCESS_ENUM.NOT_LOGIN,
|
||||
});
|
||||
userStore.logout();
|
||||
Message.success("已退出登录");
|
||||
router.push("/home");
|
||||
};
|
||||
|
||||
@@ -11,54 +11,29 @@ import {
|
||||
} from "@/utils/token";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { isApiSuccess } from "@/api/response";
|
||||
import {
|
||||
ErrorCode,
|
||||
ErrorMessages,
|
||||
isNotLoginError,
|
||||
isNoAuthError,
|
||||
isParamsError,
|
||||
isServerError,
|
||||
} from "@/types/errorCode";
|
||||
|
||||
// 扩展 Axios 请求配置类型
|
||||
declare module "axios" {
|
||||
interface InternalAxiosRequestConfig {
|
||||
_retry?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// 是否正在刷新 token
|
||||
let isRefreshing = false;
|
||||
// 存储因为 token 过期而挂起的请求
|
||||
let requests: ((token: string) => void)[] = [];
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: ENV.API_BASE_URL,
|
||||
timeout: 10000,
|
||||
withCredentials: false,
|
||||
});
|
||||
// =======================
|
||||
// 请求拦截器
|
||||
// =======================
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig<any>) => {
|
||||
// 自动携带 access token
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return error;
|
||||
}
|
||||
);
|
||||
// =======================
|
||||
// 响应拦截器
|
||||
// =======================
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
// console.log("响应: ", response);
|
||||
const data = response.data;
|
||||
/**TODO: 增加响应码处理 */
|
||||
return data;
|
||||
},
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// 处理 401 未授权情况 (Token 过期)
|
||||
// 确保不是刷新 token 的请求本身 (避免死循环)
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
!originalRequest._retry &&
|
||||
!originalRequest.url.includes("/auth/refresh")
|
||||
) {
|
||||
// 统一的刷新 token 并重试请求的函数
|
||||
const refreshTokenAndRetry = async (originalRequest: InternalAxiosRequestConfig) => {
|
||||
// 如果正在刷新,将当前请求加入队列等待
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -107,17 +82,105 @@ request.interceptors.response.use(
|
||||
console.error("Token 刷新失败:", refreshError);
|
||||
// 清除过期 token
|
||||
clearTokens();
|
||||
|
||||
// 动态导入 userStore,确保 Pinia 已初始化
|
||||
import("@/store/user").then(({ useUserStore }) => {
|
||||
const userStore = useUserStore();
|
||||
userStore.logout();
|
||||
});
|
||||
|
||||
Message.error("登录已过期,请重新登录");
|
||||
// 这里可以选择跳转到登录页,例如 window.location.href = '/user/login'
|
||||
// 建议让路由守卫或者页面自行处理未登录状态
|
||||
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
window.location.href = "/user/login";
|
||||
}, 1000);
|
||||
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: ENV.API_BASE_URL,
|
||||
timeout: 10000,
|
||||
withCredentials: false,
|
||||
});
|
||||
// =======================
|
||||
// 请求拦截器
|
||||
// =======================
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig<any>) => {
|
||||
// 自动携带 access token
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return error;
|
||||
}
|
||||
);
|
||||
// =======================
|
||||
// 响应拦截器
|
||||
// =======================
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data;
|
||||
const originalRequest = response.config;
|
||||
const code = data?.code;
|
||||
|
||||
// 未登录错误 (40100)
|
||||
if (isNotLoginError(code)) {
|
||||
clearTokens();
|
||||
import("@/store/user").then(({ useUserStore }) => {
|
||||
const userStore = useUserStore();
|
||||
userStore.logout();
|
||||
});
|
||||
Message.error(data?.message || ErrorMessages[ErrorCode.NOT_LOGIN_ERROR]);
|
||||
setTimeout(() => {
|
||||
window.location.href = "/user/login";
|
||||
}, 1000);
|
||||
return Promise.reject(new Error(data?.message || ErrorMessages[ErrorCode.NOT_LOGIN_ERROR]));
|
||||
}
|
||||
|
||||
// 无权限/token 过期错误 (40101) - 尝试刷新 token
|
||||
if (isNoAuthError(code) && !originalRequest._retry) {
|
||||
return refreshTokenAndRetry(originalRequest);
|
||||
}
|
||||
|
||||
// 参数错误 (40000)
|
||||
if (isParamsError(code)) {
|
||||
Message.warning(data?.message || ErrorMessages[ErrorCode.PARAMS_ERROR]);
|
||||
return data;
|
||||
}
|
||||
|
||||
// 服务端错误 (5xxxx)
|
||||
if (isServerError(code)) {
|
||||
Message.error(data?.message || ErrorMessages[ErrorCode.SYSTEM_ERROR]);
|
||||
return data;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// 处理 HTTP 401 未授权情况 (Token 过期)
|
||||
// 确保不是刷新 token 的请求本身 (避免死循环)
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
!originalRequest._retry &&
|
||||
!originalRequest.url.includes("/auth/refresh")
|
||||
) {
|
||||
return refreshTokenAndRetry(originalRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise,相当于“主动抛出错误”,
|
||||
* Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise,相当于"主动抛出错误",
|
||||
*
|
||||
* 让调用者能够在 .catch() 或 try/catch 中捕获这个错误。
|
||||
* 它在 异步流程、拦截器、错误处理 中非常常见。
|
||||
|
||||
@@ -39,6 +39,13 @@ export const useUserStore = defineStore("user", {
|
||||
// 手动更新用户状态
|
||||
updateUserLoginStatus(user: LoginUesr) {
|
||||
this.loginUser = user;
|
||||
},
|
||||
// 退出登录(清除登录状态)
|
||||
logout() {
|
||||
this.loginUser = {
|
||||
userName: "未登录",
|
||||
userRole: ACCESS_ENUM.NOT_LOGIN,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
80
src/types/errorCode.ts
Normal file
80
src/types/errorCode.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 错误码常量
|
||||
* 与后端保持一致
|
||||
*/
|
||||
export const ErrorCode = {
|
||||
// 成功
|
||||
SUCCESS: "0",
|
||||
|
||||
// 客户端错误 4xxxx
|
||||
PARAMS_ERROR: "40000", // 请求参数错误
|
||||
NOT_LOGIN_ERROR: "40100", // 未登录
|
||||
NO_AUTH_ERROR: "40101", // 无权限
|
||||
NOT_FOUND_ERROR: "40400", // 请求数据不存在
|
||||
FORBIDDEN_ERROR: "40300", // 禁止访问
|
||||
|
||||
// 服务端错误 5xxxx
|
||||
SYSTEM_ERROR: "50000", // 系统内部异常
|
||||
OPERATION_ERROR: "50001", // 操作失败
|
||||
API_REQUEST_ERROR: "50010", // 接口调用失败
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 错误码类型
|
||||
*/
|
||||
export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode];
|
||||
|
||||
/**
|
||||
* 错误码对应的消息映射
|
||||
* 用于当后端未返回 message 时使用
|
||||
*/
|
||||
export const ErrorMessages: Record<string, string> = {
|
||||
[ErrorCode.SUCCESS]: "操作成功",
|
||||
[ErrorCode.PARAMS_ERROR]: "请求参数错误",
|
||||
[ErrorCode.NOT_LOGIN_ERROR]: "未登录,请先登录",
|
||||
[ErrorCode.NO_AUTH_ERROR]: "无权限访问",
|
||||
[ErrorCode.NOT_FOUND_ERROR]: "请求的数据不存在",
|
||||
[ErrorCode.FORBIDDEN_ERROR]: "禁止访问",
|
||||
[ErrorCode.SYSTEM_ERROR]: "系统内部异常,请稍后重试",
|
||||
[ErrorCode.OPERATION_ERROR]: "操作失败,请稍后重试",
|
||||
[ErrorCode.API_REQUEST_ERROR]: "接口调用失败,请稍后重试",
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为成功错误码
|
||||
*/
|
||||
export const isSuccessCode = (code: string | number | undefined): boolean => {
|
||||
return code === ErrorCode.SUCCESS || code === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为未登录错误码
|
||||
*/
|
||||
export const isNotLoginError = (code: string | number | undefined): boolean => {
|
||||
return code === ErrorCode.NOT_LOGIN_ERROR;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为无权限错误码
|
||||
*/
|
||||
export const isNoAuthError = (code: string | number | undefined): boolean => {
|
||||
return code === ErrorCode.NO_AUTH_ERROR;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为参数错误
|
||||
*/
|
||||
export const isParamsError = (code: string | number | undefined): boolean => {
|
||||
return code === ErrorCode.PARAMS_ERROR;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为服务端错误 (5xxxx)
|
||||
*/
|
||||
export const isServerError = (code: string | number | undefined): boolean => {
|
||||
return (
|
||||
code === ErrorCode.SYSTEM_ERROR ||
|
||||
code === ErrorCode.OPERATION_ERROR ||
|
||||
code === ErrorCode.API_REQUEST_ERROR
|
||||
);
|
||||
};
|
||||
@@ -54,7 +54,7 @@ import { updateUserAvatar } from '@/api/auth/user';
|
||||
import { isApiSuccess } from '@/api/response';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconCamera } from '@arco-design/web-vue/es/icon';
|
||||
import SparkMD5 from 'spark-md5';
|
||||
import * as SparkMD5 from 'spark-md5';
|
||||
import type { RequestOption } from '@arco-design/web-vue/es/upload/interfaces';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
Reference in New Issue
Block a user