优化报销管理增加挂靠

This commit is contained in:
hero920103 committed 2026-11-06 01:13:28 +08:00
1 parent 7b1c94b5ff
commit ee49ef15b5
24 files changed
+1267 -70

No files matched your search

+66 -5
View File
@@ -45,6 +45,51 @@ export function hasAuthBase() {
return !!AUTH_BASE;
}
// 认证「交接页」:正在用 code / ticket 兑换令牌。此时 localStorage 里还是
// 上一次的旧 token,页面启动期的请求一旦 401,自动恢复逻辑会把刚换到的新
// token 清掉并重新跳认证中心,形成「登录成功又回登录页」的循环。
const HANDOFF_PATHS = ["/auth/callback", "/app-login", "/mp-login"];
const REDIRECT_LOG_KEY = "auth_authorize_redirects";
const LOOP_WINDOW_MS = 15000;
const LOOP_MAX = 3;
export function isAuthHandoff() {
const hash = window.location.hash || "";
const hashPath = hash.startsWith("#") ? hash.slice(1).split("?")[0] : "";
if (HANDOFF_PATHS.includes(hashPath)) return true;
if (window.location.pathname === "/auth-callback.html") return true;
// 兼容中转页把 code/ticket 放在 hash 之前的场景;只看 query,避免业务页面
// 自身的 ?token= 等参数被误判成交接页。
return /[?&](code|ticket)=/.test(window.location.search || "");
}
// 短时间内反复发起授权说明出现了重定向环:停下并给出可诊断的错误,
// 而不是让用户一直被弹回登录界面。
function guardAuthorizeLoop() {
const now = Date.now();
let times = [];
try {
times = JSON.parse(sessionStorage.getItem(REDIRECT_LOG_KEY) || "[]");
} catch (e) {
times = [];
}
times = (Array.isArray(times) ? times : []).filter((t) => now - Number(t) < LOOP_WINDOW_MS);
if (times.length >= LOOP_MAX) {
throw new Error(
"短时间内多次跳转统一登录,已停止自动跳转以避免循环;请检查认证中心与回跳白名单配置后重试"
);
}
times.push(now);
try {
sessionStorage.setItem(REDIRECT_LOG_KEY, JSON.stringify(times));
} catch (e) {
// 隐私模式下 sessionStorage 不可写,忽略即可
}
}
// 计数按时间窗口自然过期,不在换码成功后清零:否则「换码成功 → 401 → 又跳
// 认证中心」这类循环每次都会把计数抹掉,保护永远无法触发。
// ---------------------------------------------------------------- PKCE
function randomString(len = 64) {
@@ -108,6 +153,7 @@ export async function redirectToAuthorize() {
if (!AUTH_BASE) {
throw new Error("统一认证地址未配置:请检查 VITE_AUTH_BASE 或 VITE_API_BASE_URL");
}
guardAuthorizeLoop();
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem("pkce_verifier", verifier);
const state = randomString(24);
@@ -132,6 +178,12 @@ export async function redirectToAuthorize() {
/** 处理认证中心回跳:用 code 换令牌 */
export async function handleAuthorizeCallback(query = {}) {
// 交接页加载期间,启动期请求是用旧 token 发出的。先清掉旧登录态,
// 避免并发 401 续期拿旧 refresh_token 触发「重放吊销」把新会话一起废掉,
// 也避免旧 userInfo 被误当成新会话的用户信息。
clearTokens();
localStorage.removeItem("userInfo");
const code = query.code;
const state = query.state;
const savedState = sessionStorage.getItem("oidc_state");
@@ -274,10 +326,15 @@ export async function ensureUserInfo() {
localStorage.setItem('userInfo', JSON.stringify(normalized));
return normalized;
} catch (e) {
// 令牌无效(例如迁移前的旧令牌):清掉本地状态,让调用方重新登录
clearTokens();
localStorage.removeItem('userInfo');
return null;
// 只有确认令牌失效(401)才清空登录态。userinfo 接口网络抖动 / 404 / 5xx
// 属于临时故障,一律清 token 会把刚登录成功的用户反复送回认证中心。
if (e?.status === 401 || e?.isAuthError) {
clearTokens();
localStorage.removeItem('userInfo');
return null;
}
console.warn('[auth] 获取用户信息失败,保留当前登录态:', e);
return info && info.id ? info : null;
}
}
@@ -298,7 +355,11 @@ export async function fetchUserInfo() {
// 续期失败:保持原响应,交给调用方按「未登录」处理
}
}
if (!res.ok) throw new Error("获取用户信息失败");
if (!res.ok) {
const err = new Error(`获取用户信息失败(${res.status})`);
err.status = res.status;
throw err;
}
return res.json();
}