refactor(frontend): merge authserver web login

This commit is contained in:
mac
2026-07-15 14:47:42 +08:00
parent 6af511ed0b
commit 81d2a8a60b
23 changed files with 178 additions and 1115 deletions
+23 -1
View File
@@ -3,7 +3,29 @@
</template>
<script setup lang="ts">
// 根组件
import { onMounted } from 'vue'
import { authApi } from '@/api/auth'
import { getToken, removeToken } from '@/utils/auth'
import { redirectToSSO } from '@/utils/sso'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
onMounted(async () => {
const token = getToken()
if (!token) {
return
}
try {
const { data } = await authApi.getUserInfo()
authStore.setAuth(token, data)
} catch {
authStore.clearAuth()
removeToken()
redirectToSSO()
}
})
</script>
<style>
+25 -40
View File
@@ -1,4 +1,5 @@
import type { ApiResponse } from '@/types/api'
import { getToken } from '@/utils/auth'
export interface LoginRequest {
username: string
@@ -19,53 +20,37 @@ export interface LoginResponse {
user: UserInfo
}
// Mock 数据
const mockUser: UserInfo = {
id: 1,
username: 'admin',
display_name: '王晓东',
email: 'admin@qiniu.com',
business_line: 'kodo',
}
export const authApi = {
// 登录
login(_data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
// MVP 阶段使用 Mock 数据
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 0,
message: 'success',
data: {
token: 'mock-jwt-token-' + Date.now(),
expires_in: 3600,
user: mockUser,
},
})
}, 500)
})
return Promise.reject(new Error('password login is disabled'))
},
// 登出
logout(): Promise<ApiResponse<null>> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ code: 0, message: 'success', data: null })
}, 200)
})
return Promise.resolve({ code: 0, message: 'success', data: null })
},
// 获取用户信息
getUserInfo(): Promise<ApiResponse<UserInfo>> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 0,
message: 'success',
data: mockUser,
})
}, 200)
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
const token = getToken()
const response = await fetch('/auth/api/v1/users/me', {
headers: {
Accept: 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
})
const data = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`)
}
return {
code: 0,
message: 'success',
data: {
id: data.id,
username: data.username,
display_name: data.display_name || data.username,
email: data.email || '',
business_line: data.business_line || '',
},
}
},
}
+2 -1
View File
@@ -1,5 +1,6 @@
import axios, { type AxiosResponse, type AxiosError } from 'axios'
import { getToken, removeToken } from '@/utils/auth'
import { redirectToSSO } from '@/utils/sso'
import { ElMessage } from 'element-plus'
import type { ApiResponse } from '@/types/api'
import { codeMessageMap } from '@/types/api'
@@ -60,7 +61,7 @@ request.interceptors.response.use(
if (status === 401) {
removeToken()
window.location.href = '/login'
redirectToSSO()
ElMessage.error(backendMessage || '未授权,请重新登录')
} else {
const message = backendMessage || httpMessageMap[status] || `请求失败 (${status})`
+51 -24
View File
@@ -1,4 +1,6 @@
import type { ApiResponse } from '@/types/api'
import { getToken, removeToken } from '@/utils/auth'
import { redirectToSSO } from '@/utils/sso'
export interface Subsystem {
id: number
@@ -15,7 +17,6 @@ export interface SSOResponse {
expires_in: number
}
// Mock 数据
const mockSubsystems: Subsystem[] = [
{
id: 1,
@@ -74,33 +75,59 @@ const mockSubsystems: Subsystem[] = [
]
export const subsystemApi = {
// 获取子系统列表
getSubsystems(): Promise<ApiResponse<Subsystem[]>> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 0,
message: 'success',
data: mockSubsystems,
})
}, 300)
return Promise.resolve({
code: 0,
message: 'success',
data: mockSubsystems,
})
},
// 获取 SSO 跳转 URL
getSSOUrl(id: number): Promise<ApiResponse<SSOResponse>> {
return new Promise((resolve) => {
setTimeout(() => {
const subsystem = mockSubsystems.find((s) => s.id === id)
resolve({
code: 0,
message: 'success',
data: {
sso_url: subsystem?.url + '/sso/callback?code=mock-code&state=mock-state',
expires_in: 300,
},
})
}, 200)
async getSSOUrl(id: number): Promise<ApiResponse<SSOResponse>> {
const subsystem = mockSubsystems.find((s) => s.id === id)
if (!subsystem) {
throw new Error('subsystem not found')
}
if (subsystem.name !== 'Wayne' && subsystem.name !== 'CloudDM') {
return {
code: 0,
message: 'success',
data: {
sso_url: subsystem.url,
expires_in: 300,
},
}
}
const token = getToken()
const openApp = subsystem.name === 'CloudDM' ? 'clouddm' : 'wayne'
const path = subsystem.name === 'CloudDM' ? '/auth/api/v1/clouddm/login' : '/auth/api/v1/wayen/login'
const response = await fetch(path, {
headers: {
Accept: 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
credentials: 'include',
})
const data = await response.json().catch(() => ({}))
if (response.status === 401) {
removeToken()
redirectToSSO(openApp)
throw new Error('unauthorized')
}
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`)
}
if (!data.target_url) {
throw new Error(`${subsystem.name} 跳转地址为空`)
}
return {
code: 0,
message: 'success',
data: {
sso_url: data.target_url,
expires_in: 300,
},
}
},
}
+4 -2
View File
@@ -52,13 +52,15 @@ interface Column {
class?: string
}
defineProps<{
withDefaults(defineProps<{
title: string
columns: Column[]
data: any[]
total: number
pageSize?: number
}>()
}>(), {
pageSize: 10,
})
const statusMap: Record<string, string> = {
success: '成功',
+1 -1
View File
@@ -64,7 +64,7 @@ const description = computed(() => {
const handleClick = async () => {
if (props.system.sso_enabled) {
const { data } = await subsystemApi.getSSOUrl(props.system.id)
window.open(data.sso_url, '_blank')
window.location.assign(data.sso_url)
} else {
window.open(props.system.url, '_blank')
}
+3 -4
View File
@@ -1,9 +1,8 @@
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { authApi } from '@/api/auth'
import { redirectToSSO } from '@/utils/sso'
export function useAuth() {
const router = useRouter()
const authStore = useAuthStore()
const login = async (username: string, password: string) => {
@@ -16,12 +15,12 @@ export function useAuth() {
const logout = async () => {
await authApi.logout()
authStore.clearAuth()
router.push('/login')
redirectToSSO()
}
const checkAuth = () => {
if (!authStore.isLoggedIn()) {
router.push('/login')
redirectToSSO()
return false
}
return true
+4 -1
View File
@@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import { getToken } from '@/utils/auth'
import { consumeSSOToken, redirectToSSO } from '@/utils/sso'
const router = createRouter({
history: createWebHistory(),
@@ -98,10 +99,12 @@ const router = createRouter({
// 路由守卫
router.beforeEach((to, _from, next) => {
consumeSSOToken()
const token = getToken()
if (to.meta.requiresAuth !== false && !token) {
next('/login')
redirectToSSO()
return
} else if (to.path === '/login' && token) {
next('/')
} else {
+2 -2
View File
@@ -1,5 +1,5 @@
const TOKEN_KEY = 'xinfra_token'
const USER_KEY = 'xinfra_user'
const TOKEN_KEY = 'authserver_token'
const USER_KEY = 'authserver_user'
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
+42
View File
@@ -0,0 +1,42 @@
import { setToken } from '@/utils/auth'
const SSO_LOGIN_PATH = '/auth/api/v1/login/internal-sso'
export function relayState(openApp = ''): string {
const url = new URL(window.location.href)
url.searchParams.delete('sso_token')
url.searchParams.delete('open_app')
if (openApp) {
url.searchParams.set('open_app', openApp)
}
return `${url.pathname}${url.search}${url.hash}` || '/'
}
export function redirectToSSO(openApp = ''): void {
window.location.assign(`${SSO_LOGIN_PATH}?relay_state=${encodeURIComponent(relayState(openApp))}`)
}
export function consumeSSOToken(): string {
const url = new URL(window.location.href)
const token = url.searchParams.get('sso_token') || ''
if (!token) {
return ''
}
setToken(token)
url.searchParams.delete('sso_token')
window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`)
return token
}
export function consumeOpenApp(): string {
const url = new URL(window.location.href)
const openApp = url.searchParams.get('open_app') || ''
if (!openApp) {
return ''
}
url.searchParams.delete('open_app')
window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`)
return openApp
}
+7 -73
View File
@@ -7,45 +7,11 @@
xinfra
</div>
<h2>统一基础设施平台</h2>
<p>LDAP 账号登录</p>
<p>正在跳转到 SSO 登录</p>
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
@submit.prevent="handleLogin"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
size="large"
show-password
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
style="width: 100%"
>
登录
</el-button>
</el-form-item>
</el-form>
<el-button type="primary" size="large" style="width: 100%" @click="redirectToSSO()">
重新跳转
</el-button>
<div class="login-footer">
<p>统一 LDAP 账号,同账号同密码</p>
</div>
@@ -54,42 +20,10 @@
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { onMounted } from 'vue'
import { redirectToSSO } from '@/utils/sso'
const router = useRouter()
const { login } = useAuth()
const formRef = ref<FormInstance>()
const loading = ref(false)
const form = reactive({
username: '',
password: '',
})
const rules: FormRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
}
const handleLogin = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
loading.value = true
try {
await login(form.username, form.password)
ElMessage.success('登录成功')
router.push('/')
} catch {
// 错误已由 request 拦截器统一处理
} finally {
loading.value = false
}
}
onMounted(() => redirectToSSO())
</script>
<style scoped>
@@ -20,6 +20,7 @@
import { ref, onMounted } from 'vue'
import { subsystemApi, type Subsystem } from '@/api/subsystem'
import SubsystemCard from '@/components/SubsystemCard.vue'
import { consumeOpenApp } from '@/utils/sso'
const subsystems = ref<Subsystem[]>([])
@@ -27,6 +28,15 @@ onMounted(async () => {
try {
const { data } = await subsystemApi.getSubsystems()
subsystems.value = data
const openApp = consumeOpenApp()
if (openApp) {
const targetName = openApp === 'clouddm' ? 'CloudDM' : 'Wayne'
const target = data.find((item) => item.name === targetName)
if (target) {
const response = await subsystemApi.getSSOUrl(target.id)
window.location.assign(response.data.sso_url)
}
}
} catch {
// 错误已由 request 拦截器统一处理
}
+4
View File
@@ -23,6 +23,10 @@ export default defineConfig({
server: {
port: 3000,
proxy: {
'/auth': {
target: 'http://localhost:8083',
changeOrigin: true,
},
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,