fix: 修复 OIDC 用户预配与跨标签页登录态
增加受控 JIT 本地用户投影,并使用 HttpOnly Cookie 在新标签页恢复认证中心登录态。补充错误语义、安全校验、自动化测试与回滚配置。
This commit is contained in:
+80
-14
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user