sso优化
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(true)
|
||||
const actionLoading = ref(false)
|
||||
const sso = reactive({
|
||||
enabled: false,
|
||||
providerDisplayName: '学校统一身份认证',
|
||||
isBound: false,
|
||||
callbackUrl: '',
|
||||
})
|
||||
|
||||
const ssoErrors: Record<string, string> = {
|
||||
binding_intent_expired: '绑定请求已失效,请重新发起绑定。',
|
||||
identity_already_bound: '该统一身份账号已经绑定其他教务系统账号。',
|
||||
account_already_bound: '当前教务系统账号已经绑定其他统一身份账号。',
|
||||
account_disabled: '当前教务系统账号已停用或锁定。',
|
||||
account_link_failed: '统一身份账户绑定失败,请重新尝试。',
|
||||
configuration_error: '统一身份认证回调地址配置不正确,请联系管理员。',
|
||||
}
|
||||
|
||||
async function loadAccount() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await http.get('/auth/sso/account')
|
||||
sso.enabled = Boolean(data.enabled)
|
||||
sso.providerDisplayName = String(data.providerDisplayName || sso.providerDisplayName)
|
||||
sso.isBound = Boolean(data.isBound)
|
||||
sso.callbackUrl = String(data.callbackUrl || '')
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startBinding() {
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const { data } = await http.post('/auth/sso/prepare-binding')
|
||||
window.location.assign(String(data.loginUrl))
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function unbind() {
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(
|
||||
'解绑后将不能再使用当前统一身份账号登录。请输入教务系统密码确认。',
|
||||
'解除统一身份绑定',
|
||||
{
|
||||
inputType: 'password',
|
||||
inputPlaceholder: '请输入教务系统密码',
|
||||
inputValidator: (value) => Boolean(value) || '密码不能为空',
|
||||
confirmButtonText: '确认解绑',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
},
|
||||
)
|
||||
actionLoading.value = true
|
||||
await http.post('/auth/sso/unbind', { password: result.value })
|
||||
ElMessage.success('统一身份账户已解绑。')
|
||||
await loadAccount()
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const ssoError = String(route.query.ssoError ?? '')
|
||||
if (ssoError) {
|
||||
ElMessage.error(ssoErrors[ssoError] ?? '统一身份账户绑定失败,请重新尝试。')
|
||||
await router.replace('/account')
|
||||
}
|
||||
await loadAccount()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="account-page">
|
||||
<header class="account-heading">
|
||||
<div>
|
||||
<span class="eyebrow">ACCOUNT & SECURITY</span>
|
||||
<h1>个人账户</h1>
|
||||
<p>管理您的登录身份与单点登录绑定。</p>
|
||||
</div>
|
||||
<div class="account-avatar">{{ auth.user?.displayName?.slice(0, 1) ?? '用' }}</div>
|
||||
</header>
|
||||
|
||||
<div class="account-grid">
|
||||
<article class="account-card identity-card">
|
||||
<div class="card-title">
|
||||
<div>
|
||||
<span>基本信息</span>
|
||||
<h2>{{ auth.user?.displayName }}</h2>
|
||||
</div>
|
||||
<el-tag type="success" effect="light">账号已启用</el-tag>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>登录账号</dt>
|
||||
<dd>{{ auth.user?.userName }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>系统角色</dt>
|
||||
<dd class="role-list">
|
||||
<el-tag v-for="role in auth.user?.roles" :key="role" effect="plain">{{ role }}</el-tag>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>数据范围</dt>
|
||||
<dd>{{ auth.user?.effectiveDataScope }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article v-loading="loading" class="account-card sso-card">
|
||||
<div class="card-title">
|
||||
<div>
|
||||
<span>单点登录</span>
|
||||
<h2>{{ sso.providerDisplayName }}</h2>
|
||||
</div>
|
||||
<el-tag v-if="sso.isBound" type="success">已绑定</el-tag>
|
||||
<el-tag v-else-if="sso.enabled" type="info">未绑定</el-tag>
|
||||
<el-tag v-else type="warning">未启用</el-tag>
|
||||
</div>
|
||||
|
||||
<p v-if="sso.isBound" class="sso-description">
|
||||
当前教务系统账号已经关联统一身份认证。您可以直接通过 Keycloak 登录。
|
||||
</p>
|
||||
<p v-else-if="sso.enabled" class="sso-description">
|
||||
绑定后,即使统一身份用户名与教务系统账号不同,也可以直接登录当前账号。
|
||||
</p>
|
||||
<p v-else class="sso-description">管理员尚未启用统一身份认证。</p>
|
||||
|
||||
<el-button
|
||||
v-if="sso.enabled && !sso.isBound"
|
||||
type="primary"
|
||||
:loading="actionLoading"
|
||||
@click="startBinding"
|
||||
>
|
||||
绑定统一身份账户
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else-if="sso.isBound"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="actionLoading"
|
||||
@click="unbind"
|
||||
>
|
||||
解除绑定
|
||||
</el-button>
|
||||
|
||||
<details v-if="sso.enabled && sso.callbackUrl" class="callback-details">
|
||||
<summary>管理员配置参考</summary>
|
||||
<p>Keycloak Valid redirect URI 必须与下列地址完全一致:</p>
|
||||
<code>{{ sso.callbackUrl }}</code>
|
||||
</details>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.account-page { display: grid; gap: 22px; }
|
||||
.account-heading { display: flex; align-items: center; justify-content: space-between; padding: 26px 30px; border-radius: 18px; background: linear-gradient(125deg, #173a50, #176b87 65%, #2e9b91); color: white; box-shadow: 0 18px 45px rgba(23, 58, 80, .18); }
|
||||
.account-heading h1 { margin: 7px 0 5px; font-family: "STZhongsong", "Songti SC", serif; font-size: 30px; }
|
||||
.account-heading p { margin: 0; color: rgba(255,255,255,.72); }
|
||||
.account-heading .eyebrow { color: #8ee4d8; }
|
||||
.account-avatar { display: grid; place-items: center; width: 68px; height: 68px; border: 1px solid rgba(255,255,255,.34); border-radius: 22px; background: rgba(255,255,255,.12); font-size: 28px; font-weight: 700; }
|
||||
.account-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
|
||||
.account-card { min-width: 0; min-height: 310px; padding: 28px; border: 1px solid #e2e8ee; border-radius: 16px; background: white; box-shadow: 0 12px 34px rgba(35, 57, 78, .07); }
|
||||
.card-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 20px; border-bottom: 1px solid #edf0f3; }
|
||||
.card-title span { color: #73808d; font-size: 12px; }
|
||||
.card-title h2 { margin: 6px 0 0; color: #263445; font-size: 21px; }
|
||||
dl { margin: 6px 0 0; }
|
||||
dl > div { display: grid; grid-template-columns: 100px 1fr; gap: 18px; padding: 16px 0; border-bottom: 1px solid #f0f2f4; }
|
||||
dt { color: #7b8794; font-size: 13px; }
|
||||
dd { margin: 0; color: #263445; font-weight: 650; }
|
||||
.role-list { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.sso-description { min-height: 54px; margin: 22px 0; color: #65717e; line-height: 1.7; }
|
||||
.callback-details { margin-top: 24px; color: #65717e; font-size: 12px; }
|
||||
.callback-details summary { cursor: pointer; color: #315b73; font-weight: 650; }
|
||||
.callback-details p { margin: 10px 0 7px; }
|
||||
.callback-details code { display: block; overflow-wrap: anywhere; padding: 10px 12px; border-radius: 8px; background: #f4f7f9; color: #176b87; }
|
||||
@media (max-width: 1100px) {
|
||||
.account-grid { grid-template-columns: 1fr; }
|
||||
.account-heading { padding: 22px; }
|
||||
.account-avatar { width: 56px; height: 56px; border-radius: 18px; }
|
||||
.account-card { min-height: 0; padding: 22px; }
|
||||
}
|
||||
</style>
|
||||
@@ -44,6 +44,7 @@ const ssoErrors: Record<string, string> = {
|
||||
account_link_failed: '统一身份账号绑定失败,请联系管理员。',
|
||||
account_update_failed: '登录状态更新失败,请稍后重试。',
|
||||
binding_expired: '账户绑定请求已失效,请重新使用统一身份认证登录。',
|
||||
configuration_error: '统一身份认证回调地址配置不正确,请联系管理员。',
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
Reference in New Issue
Block a user