增加租户切换功能

This commit is contained in:
hero920103 committed 2026-09-29 15:38:00 +08:00
1 parent 1685b22e1e
commit 0dc5c2f075
8 files changed
+305 -60

No files matched your search

+42 -4
View File
@@ -164,8 +164,22 @@ export async function handleAuthorizeCallback(query = {}) {
return data;
}
/** 静默刷新访问令牌 */
// 刷新中的令牌请求(single-flight):并发 401 时共用同一次刷新。
// 认证中心的 refresh_token 是一次性轮换的,同一个 refresh_token 被使用两次会被
// 判定为重放攻击并吊销整个会话(见 services/auth/token.go 的 RefreshTokens),
// 因此这里必须保证「任一时刻只有一个刷新请求在飞」。
let refreshingPromise = null;
/** 静默刷新访问令牌(带并发去重) */
export async function refreshAccessToken() {
if (refreshingPromise) return refreshingPromise;
refreshingPromise = doRefreshAccessToken().finally(() => {
refreshingPromise = null;
});
return refreshingPromise;
}
async function doRefreshAccessToken() {
const refresh = getRefreshToken();
if (!refresh) throw new Error("无刷新令牌");
@@ -180,7 +194,9 @@ export async function refreshAccessToken() {
body,
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.access_token) throw new Error("刷新令牌失败");
if (!res.ok || !data.access_token) {
throw new Error(data.error_description || data.error || "刷新令牌失败");
}
setTokens(data);
return data.access_token;
}
@@ -266,9 +282,21 @@ export async function ensureUserInfo() {
/** 拉取当前登录用户(认证中心视角) */
export async function fetchUserInfo() {
const res = await fetch(`${AUTH_BASE}/userinfo`, {
let res = await fetch(`${AUTH_BASE}/userinfo`, {
headers: { Authorization: `Bearer ${getAccessToken()}` },
});
if (res.status === 401) {
// 访问令牌可能刚过期:先静默续期再重试一次,
// 否则调用方(路由守卫的 ensureUserInfo)会把用户直接踢回认证中心登录
try {
const newToken = await refreshAccessToken();
res = await fetch(`${AUTH_BASE}/userinfo`, {
headers: { Authorization: `Bearer ${newToken}` },
});
} catch (e) {
// 续期失败:保持原响应,交给调用方按「未登录」处理
}
}
if (!res.ok) throw new Error("获取用户信息失败");
return res.json();
}
@@ -284,7 +312,17 @@ export async function switchTenant(tid) {
body: JSON.stringify({ tid, client_id: CLIENT_ID }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data.code !== 200) throw new Error(data.msg || "切换企业失败");
if (!res.ok || data.code !== 200) {
const msg = data.msg || "切换企业失败";
// 会话被吊销/过期:本端已无法继续,清掉登录态回认证中心重新登录,
// 否则用户会卡在「点了没反应」的状态
if (res.status === 401 || msg.includes("会话") || msg.includes("重新登录")) {
clearTokens();
localStorage.removeItem("userInfo");
redirectToAuthorize().catch((err) => console.error("跳转统一认证失败:", err));
}
throw new Error(msg);
}
if (data.data?.tokens) setTokens(data.data.tokens);
// 切换后同步本地缓存的企业信息,避免顶部仍显示切换前的企业名
try {
+18 -1
View File
@@ -50,12 +50,28 @@ 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:
case 401: {
// 每个请求最多用新令牌重放一次,避免「401 → 刷新 → 仍 401」反复刷新
if (error.config?._tokenRetried) {
return Promise.reject(new Error(bizMsg || 'token无效,请重新登录'));
}
// 并发场景下,本请求可能带的是「旧令牌」,而别的请求已经把令牌换成新的
// (登录回跳、另一请求的静默续期都会改 localStorage)。
// 此时直接用最新令牌重放即可,既省掉一次刷新,也不会误报登录失效。
const latestToken = localStorage.getItem('token') || '';
const sentToken = String(error.config?.headers?.Authorization || '').replace(/^Bearer\s+/i, '');
if (latestToken && latestToken !== sentToken) {
console.warn('[auth] 请求携带的令牌已过期,改用最新令牌重放:', error.config?.url);
error.config.headers['Authorization'] = `Bearer ${latestToken}`;
error.config._tokenRetried = true;
return service.request(error.config);
}
// 统一认证:先用 refresh_token 静默换取新令牌并重放原请求,
// 刷新失败(令牌过期/被吊销/旧本地登录遗留 token)才真正登出。
try {
const newToken = await refreshAccessToken();
error.config.headers['Authorization'] = `Bearer ${newToken}`;
error.config._tokenRetried = true;
return service.request(error.config);
} catch (e) {
// 续期失败:清空登录态并跳认证中心重新登录(只跳一次,避免死循环)
@@ -70,6 +86,7 @@ service.interceptors.response.use(
}
return Promise.reject(new Error('token无效,请重新登录'));
}
}
case 404:
console.error('请求的资源不存在');
break;