From 68542b8739b0679af25baeaf0ff5a64361d34643 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Thu, 27 Nov 2025 15:34:15 +0800 Subject: [PATCH] feat(X-Pack): Add OIDC authentication mechanism --- frontend/src/assets/svg/logo_cas.svg | 10 +- frontend/src/assets/svg/logo_oauth.svg | 4 +- frontend/src/assets/svg/logo_oidc.svg | 17 ++ frontend/src/components/layout/Person.vue | 5 +- frontend/src/components/layout/index.vue | 5 +- frontend/src/i18n/en.json | 9 +- frontend/src/i18n/ko-KR.json | 9 +- frontend/src/i18n/zh-CN.json | 9 +- frontend/src/stores/user.ts | 3 + frontend/src/views/WelcomeView.vue | 5 +- frontend/src/views/login/xpack/Cas.vue | 3 +- frontend/src/views/login/xpack/Handler.vue | 172 +++++++++++++++++- frontend/src/views/login/xpack/Oauth2.vue | 3 +- frontend/src/views/login/xpack/Oidc.vue | 53 ++++++ .../system/authentication/OidcEditor.vue | 125 ++++++------- .../src/views/system/authentication/index.vue | 2 +- 16 files changed, 345 insertions(+), 89 deletions(-) create mode 100644 frontend/src/assets/svg/logo_oidc.svg create mode 100644 frontend/src/views/login/xpack/Oidc.vue diff --git a/frontend/src/assets/svg/logo_cas.svg b/frontend/src/assets/svg/logo_cas.svg index 72242460..ad09ca8b 100644 --- a/frontend/src/assets/svg/logo_cas.svg +++ b/frontend/src/assets/svg/logo_cas.svg @@ -1,14 +1,14 @@ - + - + - - - + + + diff --git a/frontend/src/assets/svg/logo_oauth.svg b/frontend/src/assets/svg/logo_oauth.svg index 89ef68c1..aaca69e3 100644 --- a/frontend/src/assets/svg/logo_oauth.svg +++ b/frontend/src/assets/svg/logo_oauth.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/src/assets/svg/logo_oidc.svg b/frontend/src/assets/svg/logo_oidc.svg new file mode 100644 index 00000000..99569140 --- /dev/null +++ b/frontend/src/assets/svg/logo_oidc.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/layout/Person.vue b/frontend/src/components/layout/Person.vue index d7972033..dfaee22f 100644 --- a/frontend/src/components/layout/Person.vue +++ b/frontend/src/components/layout/Person.vue @@ -83,8 +83,9 @@ const savePwdHandler = () => { pwdFormRef.value?.submit() } const logout = async () => { - await userStore.logout() - router.push('/login') + if (!(await userStore.logout())) { + router.push('/login') + } } diff --git a/frontend/src/components/layout/index.vue b/frontend/src/components/layout/index.vue index de8c2ee7..f83ef983 100644 --- a/frontend/src/components/layout/index.vue +++ b/frontend/src/components/layout/index.vue @@ -236,8 +236,9 @@ const menuSelect = (e: any) => { router.push(e.index) } const logout = async () => { - await userStore.logout() - router.push('/login') + if (!(await userStore.logout())) { + router.push('/login') + } } const toSystem = () => { router.push('/system') diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 7772e794..89ffb4c5 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -737,7 +737,11 @@ "revoke_url": "Revocation URL", "oauth2_field_mapping_placeholder": "Example: {'{'}\"account\": \"OAuth2Account\", \"name\": \"OAuth2Name\", \"email\": \"email\"{'}'}", "token_auth_method": "Token auth method", - "userinfo_auth_method": "Userinfo auth method" + "userinfo_auth_method": "Userinfo auth method", + "oidc_settings": "OIDC Settings", + "metadata_url": "Metadata URL", + "realm": "Realm", + "oidc_field_mapping_placeholder": "e.g., {\"account\": \"oidcAccount\", \"name\": \"oidcName\", \"email\": \"email\"}" }, "login": { "default_login": "Default", @@ -749,7 +753,8 @@ "qr_code": "QR Code", "platform_disable": "{0} settings are not enabled!", "input_account": "Please enter account", - "redirect_2_auth": "Redirecting to {0} authentication, {1} seconds..." + "redirect_2_auth": "Redirecting to {0} authentication, {1} seconds...", + "redirect_immediately": "Redirecting immediately" }, "supplier": { "alibaba_cloud_bailian": "Alibaba Cloud Bailian", diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index cb149dd5..3c7fe3c8 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -737,7 +737,11 @@ "revoke_url": "취소 URL", "oauth2_field_mapping_placeholder": "예: {'{'}\"account\": \"OAuth2Account\", \"name\": \"OAuth2Name\", \"email\": \"email\"{'}'}", "token_auth_method": "토큰 인증 방식", - "userinfo_auth_method": "사용자 정보 인증 방식" + "userinfo_auth_method": "사용자 정보 인증 방식", + "oidc_settings": "OIDC 설정", + "metadata_url": "메타데이터 URL", + "realm": "영역", + "oidc_field_mapping_placeholder": "예: {\"account\": \"oidcAccount\", \"name\": \"oidcName\", \"email\": \"email\"}" }, "login": { "default_login": "기본값", @@ -749,7 +753,8 @@ "qr_code": "QR 코드", "platform_disable": "{0} 설정이 활성화되지 않았습니다!", "input_account": "계정을 입력해 주세요", - "redirect_2_auth": "{0} 인증으로 리디렉션 중입니다, {1}초..." + "redirect_2_auth": "{0} 인증으로 리디렉션 중입니다, {1}초...", + "redirect_immediately": "지금 이동" }, "supplier": { "alibaba_cloud_bailian": "알리바바 클라우드 바이리엔", diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 0897aa9a..7da8b245 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -737,7 +737,11 @@ "revoke_url": "撤销地址", "oauth2_field_mapping_placeholder": "例如:{'{'}\"account\": \"oauth2Account\", \"name\": \"oauth2Name\", \"email\": \"email\"{'}'}", "token_auth_method": "Token 认证方式", - "userinfo_auth_method": "用户信息认证方式" + "userinfo_auth_method": "用户信息认证方式", + "oidc_settings": "OIDC 设置", + "metadata_url": "元数据地址", + "realm": "领域", + "oidc_field_mapping_placeholder": "例如:{'{'}\"account\": \"oidcAccount\", \"name\": \"oidcName\", \"email\": \"email\"{'}'}" }, "login": { "default_login": "默认", @@ -749,7 +753,8 @@ "qr_code": "二维码", "platform_disable": "{0}设置未开启!", "input_account": "请输入账号", - "redirect_2_auth": "正在跳转至 {0} 认证,{1} 秒..." + "redirect_2_auth": "正在跳转至 {0} 认证,{1} 秒...", + "redirect_immediately": "立即跳转" }, "supplier": { "alibaba_cloud_bailian": "阿里云百炼", diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index d3bc3cd4..ce9bd4c1 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -91,12 +91,15 @@ export const UserStore = defineStore('user', { if (res) { window.location.href = res window.open(res, '_self') + return res } if (getQueryString('code') && getQueryString('state')?.includes('oauth2_state')) { const logout_url = location.origin + location.pathname + '#/login' window.location.href = logout_url window.open(res, logout_url) + return logout_url } + return null }, async info() { diff --git a/frontend/src/views/WelcomeView.vue b/frontend/src/views/WelcomeView.vue index fb382c86..e8813fd2 100644 --- a/frontend/src/views/WelcomeView.vue +++ b/frontend/src/views/WelcomeView.vue @@ -18,8 +18,9 @@ const router = useRouter() const userStore = useUserStore() const logout = async () => { - await userStore.logout() - router.push('/login') + if (!(await userStore.logout())) { + router.push('/login') + } } diff --git a/frontend/src/views/login/xpack/Cas.vue b/frontend/src/views/login/xpack/Cas.vue index b42b086e..96f4747e 100644 --- a/frontend/src/views/login/xpack/Cas.vue +++ b/frontend/src/views/login/xpack/Cas.vue @@ -31,6 +31,7 @@ const execute = () => { font-size: 32px; border: 1px solid #dee0e3; border-radius: 50%; + color: var(--ed-color-primary); } display: flex; align-items: center; @@ -41,7 +42,7 @@ const execute = () => { margin-top: 8px; color: #000; text-align: center; - font-family: var(--de-custom_font, 'PingFang'); + font-family: var(--ed-color-primary, 'PingFang'); font-size: 12px; font-style: normal; font-weight: 400; diff --git a/frontend/src/views/login/xpack/Handler.vue b/frontend/src/views/login/xpack/Handler.vue index 667a7c68..7261a6a4 100644 --- a/frontend/src/views/login/xpack/Handler.vue +++ b/frontend/src/views/login/xpack/Handler.vue @@ -19,8 +19,8 @@ :qrcode="loginCategory.qrcode" :ldap="loginCategory.ldap" @status-change="qrStatusChange" - /> - --> + /> --> + @@ -29,6 +29,24 @@ + + + + + diff --git a/frontend/src/views/system/authentication/OidcEditor.vue b/frontend/src/views/system/authentication/OidcEditor.vue index 3f868a49..6adf7a20 100644 --- a/frontend/src/views/system/authentication/OidcEditor.vue +++ b/frontend/src/views/system/authentication/OidcEditor.vue @@ -8,25 +8,16 @@ const { t } = useI18n() const dialogVisible = ref(false) const loadingInstance = ref | null>(null) const oidcForm = ref() -interface OidcForm { - clientId?: string - clientSecret?: string - discovery?: string - redirectUri?: string - realm?: string - scope?: string - usePkce?: boolean - mapping?: string -} + +const id = ref(null) const state = reactive({ - form: reactive({ - clientId: '', - clientSecret: '', - discovery: '', - redirectUri: '', + form: reactive({ + client_id: '', + client_secret: '', + metadata_url: '', + redirect_uri: '', realm: '', scope: '', - usePkce: false, mapping: '', }), }) @@ -48,13 +39,14 @@ const validateMapping = (rule, value, callback) => { } try { JSON.parse(value) - } catch (e) { + } catch (e: any) { + console.error(e) callback(new Error(t('system.in_json_format'))) } callback() } const rule = reactive({ - clientId: [ + client_id: [ { required: true, message: t('common.require'), @@ -67,7 +59,7 @@ const rule = reactive({ trigger: 'blur', }, ], - clientSecret: [ + client_secret: [ { required: true, message: t('common.require'), @@ -80,7 +72,7 @@ const rule = reactive({ trigger: 'blur', }, ], - redirectUri: [ + redirect_uri: [ { required: true, message: t('common.require'), @@ -94,7 +86,7 @@ const rule = reactive({ }, { required: true, validator: validateUrl, trigger: 'blur' }, ], - discovery: [ + metadata_url: [ { required: true, message: t('common.require'), @@ -134,28 +126,23 @@ const rule = reactive({ trigger: 'blur', }, ], - usePkce: [ - { - required: true, - message: t('common.require'), - trigger: 'change', - }, - ], + mapping: [{ required: false, validator: validateMapping, trigger: 'blur' }], }) const edit = () => { showLoading() request - .get('/setting/authentication/info/oidc') + .get('/system/authentication/2') .then((res) => { - const resData = res as Partial - ;(Object.keys(resData) as (keyof OidcForm)[]).forEach((key) => { - const value = resData[key] - if (value !== undefined) { - state.form[key] = value as any - } - }) + if (!res?.config) { + return + } + id.value = res.id + const data = JSON.parse(res.config) + for (const key in data) { + state.form[key] = data[key] as any + } }) .finally(() => { closeLoading() @@ -169,7 +156,15 @@ const submitForm = async (formEl: FormInstance | undefined) => { await formEl.validate((valid) => { if (valid) { const param = { ...state.form } - const method = request.post('/setting/authentication/save/oidc', param) + const data = { + id: 2, + type: 2, + config: JSON.stringify(param), + name: 'oidc', + } + const method = id.value + ? request.put('/system/authentication', data) + : request.post('/system/authentication', data) showLoading() method .then((res) => { @@ -190,6 +185,7 @@ const submitForm = async (formEl: FormInstance | undefined) => { const resetForm = (formEl: FormInstance | undefined) => { if (!formEl) return formEl.resetFields() + id.value = null dialogVisible.value = false } @@ -207,16 +203,21 @@ const closeLoading = () => { } const validate = () => { - const url = '/setting/authentication/validate/oidc' - const data = state.form + const url = '/system/authentication/status' + const config_data = state.form + const data = { + type: 2, + name: 'oidc', + config: JSON.stringify(config_data), + } showLoading() request - .post(url, data) + .patch(url, data) .then((res) => { - if (res === 'true') { - ElMessage.success(t('commons.test_connect') + t('report.last_status_success')) + if (res) { + ElMessage.success(t('ds.connection_success')) } else { - ElMessage.error(t('commons.test_connect') + t('report.last_status_fail')) + ElMessage.error(t('ds.connection_failed')) } }) .finally(() => { @@ -233,7 +234,7 @@ defineExpose({