fix(auth): 修复令牌过期处理和会话失效通知机制

- 移除 VITE_SERVICE_LOGOUT_CODES 中的 1002023000 状态码
- 将 VITE_SERVICE_EXPIRED_TOKEN_CODES 从 1002023001 改为 1002023000
- 修改 fetchRefreshToken 函数使用 params 传递 refreshToken 并设置 skipAuth
- 添加 skipAuth 配置选项避免给公开接口带上过期 access 头
- 实现 notifySessionExpired 函数确保并发请求只弹一次会话失效提示
- 在登录成功后复位会话失效标志以支持下次正常提示
- 更新 handleExpiredRequest 使用 refreshTokenPromise 替代 refreshTokenFn
This commit is contained in:
2026-05-18 08:29:51 +08:00
parent 543d1a59a9
commit 387eb41412
11 changed files with 187 additions and 179 deletions

View File

@@ -114,7 +114,9 @@ export async function fetchRefreshToken(refreshToken: string): Promise<ServiceRe
method: 'post',
params: { refreshToken },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
skipAuth: true
skipAuth: true,
suppressErrorMessage: true,
skipTokenRefresh: true
});
if (result.error || !result.data) {

View File

@@ -10,6 +10,10 @@ declare module 'axios' {
* 避免给它们带上过期 access 头被网关拦截。
*/
skipAuth?: boolean;
/** 请求失败时不走通用错误 toast由调用方自行收敛提示。 */
suppressErrorMessage?: boolean;
/** 请求失败命中过期 access code 时,不再触发 refresh-token 流程。 */
skipTokenRefresh?: boolean;
}
}

View File

@@ -0,0 +1,32 @@
export const SESSION_EXPIRED_MESSAGE = '登录已失效,请重新登录';
export interface ErrorMessageSuppressOptions {
backendErrorCode: string;
suppressErrorMessage?: boolean;
logoutCodes: string[];
modalLogoutCodes: string[];
expiredTokenCodes: string[];
}
export interface BackendFailDeferOptions {
suppressErrorMessage?: boolean;
skipTokenRefresh?: boolean;
}
export function parseServiceCodes(codes?: string) {
return codes?.split(',').filter(Boolean) || [];
}
export function shouldDeferBackendFailToCaller(options: BackendFailDeferOptions) {
return Boolean(options.suppressErrorMessage && options.skipTokenRefresh);
}
export function shouldSuppressErrorMessage(options: ErrorMessageSuppressOptions) {
if (options.suppressErrorMessage) {
return true;
}
const handledCodes = [...options.logoutCodes, ...options.modalLogoutCodes, ...options.expiredTokenCodes];
return handledCodes.includes(options.backendErrorCode);
}

View File

@@ -5,6 +5,7 @@ import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
import { $t } from '@/locales';
import { applyApiEncrypt } from './api-encrypt';
import { parseServiceCodes, shouldDeferBackendFailToCaller, shouldSuppressErrorMessage } from './error-message';
import { getAuthorization, handleExpiredRequest, notifySessionExpired, showErrorMsg } from './shared';
import { withDedupe } from './dedupe';
import type { RequestInstanceState } from './type';
@@ -48,6 +49,15 @@ export const request = withDedupe(
const authStore = useAuthStore();
const responseCode = String(response.data.code);
if (
shouldDeferBackendFailToCaller({
suppressErrorMessage: response.config.suppressErrorMessage,
skipTokenRefresh: response.config.skipTokenRefresh
})
) {
return null;
}
function handleLogout() {
authStore.resetStore();
}
@@ -61,14 +71,14 @@ export const request = withDedupe(
// 当后端返回码命中 `logoutCodes` 时,表示登录态已失效,需要提示后退出登录
// 走 notifySessionExpired 而不是裸 resetStore保证并发请求只弹一次 toast、只清一次状态
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
if (logoutCodes.includes(responseCode)) {
notifySessionExpired();
return null;
}
// 当后端返回码命中 `modalLogoutCodes` 时,表示通过弹窗提示后再退出登录
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
if (modalLogoutCodes.includes(responseCode) && !request.state.errMsgStack?.includes(response.data.msg)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
@@ -92,8 +102,13 @@ export const request = withDedupe(
// 当后端返回码命中 `expiredTokenCodes` 时,表示 token 已过期,需要刷新 token
// `refreshToken` 接口不能再返回 `expiredTokenCodes` 中的错误码,否则会形成死循环,应返回 `logoutCodes` 或 `modalLogoutCodes`
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
if (expiredTokenCodes.includes(responseCode)) {
if (response.config.skipTokenRefresh) {
notifySessionExpired();
return null;
}
const success = await handleExpiredRequest(request.state);
if (success) {
const Authorization = getAuthorization();
@@ -117,15 +132,19 @@ export const request = withDedupe(
backendErrorCode = String(error.response?.data?.code || '');
}
// 这类错误信息已经通过弹窗展示,不再重复提示
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(backendErrorCode)) {
return;
}
// token 过期时会自动刷新并重试请求,这里无需额外提示
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
if (expiredTokenCodes.includes(backendErrorCode)) {
const suppressErrorMessage = Boolean(error.config?.suppressErrorMessage);
const logoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_LOGOUT_CODES);
const modalLogoutCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES);
const expiredTokenCodes = parseServiceCodes(import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES);
if (
shouldSuppressErrorMessage({
backendErrorCode,
suppressErrorMessage,
logoutCodes,
modalLogoutCodes,
expiredTokenCodes
})
) {
return;
}

View File

@@ -1,6 +1,7 @@
import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage';
import { fetchRefreshToken } from '../api';
import { SESSION_EXPIRED_MESSAGE } from './error-message';
import type { RequestInstanceState } from './type';
export function getAuthorization() {
@@ -51,7 +52,7 @@ export function notifySessionExpired() {
if (sessionExpiredNotified) return;
sessionExpiredNotified = true;
window.$message?.error('登录已过期,请重新登录');
window.$message?.error(SESSION_EXPIRED_MESSAGE);
const { resetStore } = useAuthStore();
resetStore();