修复bug
This commit is contained in:
1 parent
1850ac640e
commit
892ba5886d
20 files changed
+1078
-178
No files matched your search
@@ -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;
|
||||
Reference in new issue
Block a user