From 50969ed175d0cc3556f27f974f323c40d3816819 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Jun 2026 10:26:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(oidc):=20=E4=BF=AE=E5=A4=8D=E8=B7=A8?= =?UTF-8?q?=E7=B3=BB=E7=BB=9FOIDC=E7=99=BB=E5=BD=95=E5=90=8E=E5=9B=9E?= =?UTF-8?q?=E5=88=B0=E6=9C=AC=E7=B3=BB=E7=BB=9F=E4=BB=8D=E9=9C=80=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E7=99=BB=E5=BD=95=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoginView 挂载时等待 OIDC 配置就绪后显式调用自动触发逻辑 - 新增 oidcAutoStart.js 工具模块,支持 autostartOidc 参数判断和 sessionStorage 防循环 - OIDC 回调成功后清理自动触发标记,避免影响下一次跨系统登录 - 已登录用户访问 /login 时按 redirect 参数跳转目标页,不再固定跳回首页 Co-Authored-By: Claude Opus 4.7 --- web/src/router/index.js | 5 +- web/src/utils/oidcAutoStart.js | 77 ++++++++++++++++++++++++++++++ web/src/views/LoginView.vue | 24 ++++++++-- web/src/views/OIDCCallbackView.vue | 4 +- 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 web/src/utils/oidcAutoStart.js diff --git a/web/src/router/index.js b/web/src/router/index.js index 73e587d9..da9bf68c 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -3,6 +3,7 @@ import AppLayout from '@/layouts/AppLayout.vue' import BlankLayout from '@/layouts/BlankLayout.vue' import { useUserStore } from '@/stores/user' import { useAgentStore } from '@/stores/agent' +import { sanitizeRedirect } from '@/utils/oidcAutoStart' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -192,9 +193,9 @@ router.beforeEach(async (to, from, next) => { return } - // 如果用户已登录但访问登录页 + // 如果用户已登录但访问登录页,按 redirect 参数跳转 if (to.path === '/login' && isLoggedIn) { - next('/') + next(sanitizeRedirect(to.query.redirect)) return } diff --git a/web/src/utils/oidcAutoStart.js b/web/src/utils/oidcAutoStart.js new file mode 100644 index 00000000..85f54e0b --- /dev/null +++ b/web/src/utils/oidcAutoStart.js @@ -0,0 +1,77 @@ +const AUTO_START_KEY = 'oidc_auto_start_attempted' + +// 对 redirect 参数进行安全校验,防止开放重定向漏洞 +// - 必须是字符串类型 +// - 必须以 / 开头(本地相对路径) +// - 不能以 // 或 \ 开头(协议相对 URL / 非标准路径) +// 不合法或缺失时返回 "/" +export function sanitizeRedirect(value) { + if (typeof value === 'string' && value.length > 0 && value[0] === '/' && value[1] !== '/' && value[1] !== '\\') { + return value + } + return '/' +} + +// 检查是否已经尝试过自动触发 OIDC(同一会话内,避免无限循环) +export function hasAutoStartAttempted() { + return sessionStorage.getItem(AUTO_START_KEY) === '1' +} + +// 标记已尝试自动触发 +export function markAutoStartAttempted() { + sessionStorage.setItem(AUTO_START_KEY, '1') +} + +// 清除自动触发尝试标记(OIDC 登录成功后调用) +export function clearAutoStartAttempt() { + sessionStorage.removeItem(AUTO_START_KEY) +} + +// 尝试自动触发 OIDC 登录 +// config: OIDC 配置对象(由调用方在外部获取) +// getOIDCLoginUrl: 获取登录 URL 的异步函数 +// 返回 true 表示已发起跳转,caller 不应继续执行后续流程 +export async function tryAutoStartOIDC(getOIDCLoginUrl, config) { + // 1. OIDC 配置未就绪或未启用 + if (!config || !config.enabled) { + return false + } + + // 2. 有 oidc_error 时不再自动触发,避免循环 + const params = new URLSearchParams(window.location.search) + if (params.has('oidc_error')) { + return false + } + + // 3. 必须有 autostartOidc 参数 + if (!params.has('autostartOidc')) { + return false + } + + // 4. 同一会话内已尝试过,不再重复 + if (hasAutoStartAttempted()) { + return false + } + + // 5. 获取 OIDC 登录 URL 并跳转 + let loginUrlResp + try { + loginUrlResp = await getOIDCLoginUrl() + } catch { + return false + } + + if (!loginUrlResp || !loginUrlResp.login_url) { + return false + } + + // 保存当前路径,登录后返回(安全校验) + const redirectPath = sanitizeRedirect(params.get('redirect')) + sessionStorage.setItem('oidc_redirect', redirectPath) + + // 标记已尝试,防止下次再自动触发 + markAutoStartAttempted() + + window.location.href = loginUrlResp.login_url + return true +} diff --git a/web/src/views/LoginView.vue b/web/src/views/LoginView.vue index 97076f82..5b35ec57 100644 --- a/web/src/views/LoginView.vue +++ b/web/src/views/LoginView.vue @@ -283,6 +283,7 @@ import { Key as KeyIcon, AlertCircle as ExclamationCircleIcon } from 'lucide-vue-next' +import { tryAutoStartOIDC, sanitizeRedirect } from '@/utils/oidcAutoStart' const router = useRouter() const route = useRoute() @@ -539,9 +540,11 @@ const checkOIDCConfig = async () => { if (config.provider_name) { oidcButtonText.value = config.provider_name } + return config } catch (error) { console.error('检查 OIDC 配置失败:', error) oidcEnabled.value = false + return null } finally { oidcChecking.value = false } @@ -614,9 +617,9 @@ const checkServerHealth = async () => { // 组件挂载时 onMounted(async () => { - // 如果已登录,跳转到首页 + // 如果已登录,按 redirect 参数跳转(不固定跳首页) if (userStore.isLoggedIn) { - router.push('/') + router.push(sanitizeRedirect(route.query.redirect)) return } @@ -631,8 +634,21 @@ onMounted(async () => { // 检查是否是首次运行 await checkFirstRunStatus() - // 检查 OIDC 配置 - checkOIDCConfig() + // 如果处于首次运行状态,不需要 OIDC 自动登录 + if (isFirstRun.value) { + return + } + + // 检查 OIDC 配置完成后,尝试自动触发 OIDC 登录(跨系统跳转场景) + const config = await checkOIDCConfig() + if (config && config.enabled) { + const autoStarted = await tryAutoStartOIDC( + async () => await authApi.getOIDCLoginUrl(), + config + ) + // 如果已发起 OIDC 跳转,页面会被重定向,不需要继续 + if (autoStarted) return + } }) // 组件卸载时清理定时器 diff --git a/web/src/views/OIDCCallbackView.vue b/web/src/views/OIDCCallbackView.vue index 88c57ce9..a1cbd98a 100644 --- a/web/src/views/OIDCCallbackView.vue +++ b/web/src/views/OIDCCallbackView.vue @@ -32,6 +32,7 @@ import { useUserStore } from '@/stores/user' import { useAgentStore } from '@/stores/agent' import { authApi } from '@/apis/auth_api' import { message } from 'ant-design-vue' +import { clearAutoStartAttempt } from '@/utils/oidcAutoStart' const router = useRouter() const route = useRoute() @@ -84,9 +85,10 @@ const handleCallback = async () => { // 显示成功消息 message.success('登录成功') - // 获取重定向路径 + // 获取重定向路径并清理 OIDC 相关标记 const redirectPath = sessionStorage.getItem('oidc_redirect') || '/' sessionStorage.removeItem('oidc_redirect') + clearAutoStartAttempt() loading.value = false