修复bug

This commit is contained in:
hero920103 committed 2026-10-19 17:34:11 +08:00
1 parent 1850ac640e
commit 892ba5886d
20 files changed
+1078 -178

No files matched your search

+78 -42
View File
@@ -13,7 +13,73 @@ const service = axios.create({
timeout: 300000,
withCredentials: false // JWT 不需要 Cookie
});
// 构造鉴权失败错误:带上 status,调用方(如菜单 store)可直接按 401 判断,
// 不必依赖中文文案;否则文案一改,登录态判断就静默失效了。
function authError(message) {
const err = new Error(message || '登录状态已失效,请重新登录');
err.status = 401;
err.isAuthError = true;
return err;
}
// 清空登录态并回认证中心重新登录(只跳一次,避免并发请求反复跳转)
function redirectToLoginOnce() {
clearTokens();
localStorage.removeItem('userInfo');
if (ssoRedirecting) return;
ssoRedirecting = true;
redirectToAuthorize().catch((err) => {
console.error('跳转统一认证失败:', err);
ssoRedirecting = false;
});
}
// 401 统一处理:
// 阶段一:并发场景下,本请求带的是旧令牌,而别的请求已换成新令牌 → 直接用最新令牌重放;
// 阶段二:仍 401 → 用 refresh_token 静默续期后重放一次;
// 阶段三:续期失败 / 续期后仍 401 → 清登录态回认证中心。
// 「重放」与「续期」是两个独立阶段:前者不消耗 refresh_token,只挡一次;
// 否则令牌真的过期时就永远等不到续期,只会一路报「无效的token」。
async function recoverAuth(config, bizMsg) {
const latestToken = localStorage.getItem('token') || '';
const sentToken = String(config?.headers?.Authorization || '').replace(/^Bearer\s+/i, '');
if (!config?._replayedWithLatest && latestToken && latestToken !== sentToken) {
console.warn('[auth] 请求携带的令牌已过期,改用最新令牌重放:', config?.url);
config.headers['Authorization'] = `Bearer ${latestToken}`;
config._replayedWithLatest = true;
return service.request(config);
}
if (config?._refreshed) {
// 续期拿到的令牌仍被判定过期/无效:继续留在页面只会一路报错,直接回认证中心
redirectToLoginOnce();
return Promise.reject(authError(bizMsg));
}
try {
const newToken = await refreshAccessToken();
config.headers['Authorization'] = `Bearer ${newToken}`;
config._refreshed = true;
return service.request(config);
} catch (e) {
redirectToLoginOnce();
return Promise.reject(authError('登录状态已失效,请重新登录'));
}
}
// 令牌类错误提示。后端部分接口(全局鉴权中间件为 warn 模式时控制器自行鉴权)
// 会在 HTTP 200 的 body 里返回 code:401,需要按提示词识别。
// 不能把所有 body code:401 都当令牌问题:个别接口(如新增用户时校验原密码)
// 也用 401 表示业务失败,那类响应重放没有意义,更不能因此把用户登出。
const TOKEN_ERROR_HINTS = ['token已过期', '无效的token', '未提供认证信息', '认证信息格式错误'];
function isTokenFailure(body) {
if (!body || Number(body.code) !== 401) return false;
const msg = String(body.msg || body.message || '');
return TOKEN_ERROR_HINTS.some((hint) => msg.includes(hint));
}
// 请求拦截器
service.interceptors.request.use(
config => {
@@ -38,11 +104,16 @@ service.interceptors.request.use(
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
return response.data;
const data = response.data;
// 兜住 HTTP 200 + body code:401 的令牌失效响应,让它走与真实 401 相同的续期流程
if (isTokenFailure(data)) {
return recoverAuth(response.config, data.msg || data.message);
}
return data;
},
async error => {
if (error.response) {
@@ -50,43 +121,8 @@ service.interceptors.response.use(
// 避免页面只显示 "Request failed with status code xxx"
const bizMsg = error.response.data?.msg || error.response.data?.message;
switch (error.response.status) {
case 401: {
// 阶段一:并发场景下,本请求可能带的是「旧令牌」,而别的请求已经把令牌换成新的
// (登录回跳、另一请求的静默续期都会改 localStorage)。
// 此时直接用最新令牌重放,免得白跑一次续期。
const latestToken = localStorage.getItem('token') || '';
const sentToken = String(error.config?.headers?.Authorization || '').replace(/^Bearer\s+/i, '');
if (!error.config?._replayedWithLatest && latestToken && latestToken !== sentToken) {
console.warn('[auth] 请求携带的令牌已过期,改用最新令牌重放:', error.config?.url);
error.config.headers['Authorization'] = `Bearer ${latestToken}`;
error.config._replayedWithLatest = true;
return service.request(error.config);
}
// 阶段二:仍 401 → 用 refresh_token 静默续期后重放一次。
// 注意「重放」与「续期」是两个独立阶段:前者不消耗 refresh_token,
// 只挡一次;否则令牌真的过期时就永远等不到续期,只会一路报「无效的token」。
if (error.config?._refreshed) {
return Promise.reject(new Error(bizMsg || 'token无效,请重新登录'));
}
try {
const newToken = await refreshAccessToken();
error.config.headers['Authorization'] = `Bearer ${newToken}`;
error.config._refreshed = true;
return service.request(error.config);
} catch (e) {
// 续期失败:清空登录态并跳认证中心重新登录(只跳一次,避免死循环)
clearTokens();
localStorage.removeItem('userInfo');
if (!ssoRedirecting) {
ssoRedirecting = true;
redirectToAuthorize().catch((err) => {
console.error('跳转统一认证失败:', err);
ssoRedirecting = false;
});
}
return Promise.reject(new Error('token无效,请重新登录'));
}
}
case 401:
return recoverAuth(error.config, bizMsg);
case 404:
console.error('请求的资源不存在');
break;
@@ -108,5 +144,5 @@ service.interceptors.response.use(
return Promise.reject(error);
}
);
export default service;
export default service;