fix: 修复 OIDC 用户预配与跨标签页登录态

增加受控 JIT 本地用户投影,并使用 HttpOnly Cookie 在新标签页恢复认证中心登录态。补充错误语义、安全校验、自动化测试与回滚配置。
This commit is contained in:
2026-07-13 17:07:52 +08:00
parent 17b1f77e1d
commit a81a7b5200
37 changed files with 2694 additions and 179 deletions
+80 -14
View File
@@ -50,6 +50,7 @@ import {
deleteApiKey,
deleteFileStorageChannel,
deleteGatewayUser,
deleteOIDCBrowserSession,
deletePlatform,
deleteTenant,
deleteUserGroup,
@@ -87,6 +88,7 @@ import {
listUserGroups,
listUsers,
loginLocalAccount,
OIDC_BROWSER_SESSION_CREDENTIAL,
pollTaskUntilSettled,
registerLocalAccount,
rechargeUserWalletBalance,
@@ -110,8 +112,20 @@ import { LoginRequiredPanel } from './components/LoginRequiredPanel';
import { useCatalogOperations } from './hooks/useCatalogOperations';
import { usePricingRuleSetOperations } from './hooks/usePricingRuleSetOperations';
import { useRuntimePolicySetOperations } from './hooks/useRuntimePolicySetOperations';
import { persistAccessToken, readStoredAccessToken } from './lib/auth-storage';
import { completeOIDCLogin, oidcLoginEnabled, startOIDCLogin, startOIDCLogout } from './lib/oidc';
import {
persistAccessToken,
persistLegacyOIDCAccessToken,
readLegacyOIDCAccessToken,
readStoredAccessToken,
} from './lib/auth-storage';
import { activateOIDCBrowserSession, restoreOIDCBrowserSession } from './lib/oidc-browser-session';
import {
completeOIDCLogin,
oidcBrowserSessionEnabled,
oidcLoginEnabled,
startOIDCLogin,
startOIDCLogout,
} from './lib/oidc';
import { runTask } from './lib/run-task';
import { AdminPage } from './pages/AdminPage';
import { ApiDocsPage } from './pages/ApiDocsPage';
@@ -272,17 +286,45 @@ export function App() {
currentTransactionQueryKeyRef.current = transactionListRequestKey;
useEffect(() => {
void completeOIDCLogin()
.then((result) => {
if (!result) return;
persistAccessToken(result.accessToken, 'session');
setToken(result.accessToken);
let cancelled = false;
void (async () => {
const result = await completeOIDCLogin();
if (cancelled) return;
if (result) {
if (!oidcBrowserSessionEnabled()) {
persistLegacyOIDCAccessToken(result.accessToken);
setToken(result.accessToken);
applyRoute(parseAppRoute(result.returnTo));
return;
}
const credential = await activateOIDCBrowserSession(result.accessToken);
if (cancelled) return;
setToken(credential);
applyRoute(parseAppRoute(result.returnTo));
})
.catch((err) => {
setState('error');
setError(err instanceof Error ? err.message : '统一认证登录失败');
});
return;
}
if (!oidcBrowserSessionEnabled()) return;
const legacyOIDCToken = readLegacyOIDCAccessToken();
if (legacyOIDCToken) {
const credential = await activateOIDCBrowserSession(legacyOIDCToken);
if (!cancelled) setToken(credential);
return;
}
if (readStoredAccessToken() || !oidcLoginEnabled()) return;
const restored = await restoreOIDCBrowserSession();
if (!restored || cancelled) return;
setCurrentUser(restored.user);
loadedDataKeysRef.current.add('currentUser');
setToken(restored.credential);
})().catch((err) => {
if (cancelled) return;
setState('error');
setError(err instanceof Error ? err.message : '统一认证登录失败');
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
void ensureData(['health']);
@@ -402,6 +444,7 @@ export function App() {
try {
await Promise.all(requestKeys.map((key) => loadDataKey(key, nextToken)));
requestKeys.forEach((key) => loadedDataKeysRef.current.add(key));
setError('');
setState('ready');
} catch (err) {
if (handleAuthExpired(err, nextToken)) return;
@@ -1026,7 +1069,9 @@ export function App() {
const selectedApiKeySecret = selectedPlaygroundApiKeyId ? apiKeySecretsById[selectedPlaygroundApiKeyId] ?? '' : '';
const fallbackApiKeySecret = apiKeys.find((item) => Boolean(apiKeySecretsById[item.id]))?.id;
const credential = selectedApiKeySecret || (fallbackApiKeySecret ? apiKeySecretsById[fallbackApiKeySecret] : '') || apiKeySecret || token;
const credentialLabel = selectedApiKeySecret || fallbackApiKeySecret || apiKeySecret ? '本地 API Key' : '当前 Access Token';
const usingLocalAPIKey = Boolean(selectedApiKeySecret || fallbackApiKeySecret || apiKeySecret);
let credentialLabel = usingLocalAPIKey ? '本地 API Key' : '当前 Access Token';
if (!usingLocalAPIKey && token === OIDC_BROWSER_SESSION_CREDENTIAL) credentialLabel = '当前登录会话';
setCoreState('loading');
setCoreMessage('');
try {
@@ -1104,8 +1149,28 @@ export function App() {
}
async function signOut() {
const wasOIDCSession = token === OIDC_BROWSER_SESSION_CREDENTIAL;
if (wasOIDCSession) {
try {
await deleteOIDCBrowserSession();
} catch (err) {
setState('error');
setError(err instanceof Error ? err.message : '统一认证会话注销失败,请重试');
return;
}
}
const shouldEndOIDCSession = wasOIDCSession || currentUser?.source === 'oidc';
resetAuthenticatedSession();
if (await startOIDCLogout()) return;
if (shouldEndOIDCSession) {
try {
if (await startOIDCLogout()) return;
} catch {
navigatePath('/');
setState('error');
setError('Gateway 会话已注销,但统一认证退出失败');
return;
}
}
navigatePath('/');
}
@@ -1137,6 +1202,7 @@ export function App() {
}
function navigatePath(path: string) {
setError('');
if (`${window.location.pathname}${window.location.search}` !== path) {
window.history.pushState(null, '', path);
}