优化报销管理增加挂靠
This commit is contained in:
1 parent
7b1c94b5ff
commit
ee49ef15b5
24 files changed
+1267
-70
No files matched your search
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user