浏览代码

单点登录逻辑调整

lingmin_package@163.com 4 周之前
父节点
当前提交
4be51262dc
共有 11 个文件被更改,包括 199 次插入56 次删除
  1. 1 1
      .env.dev
  2. 1 1
      .env.prod
  3. 1 1
      index.html
  4. 5 0
      src/api/auth.ts
  5. 11 7
      src/api/request.ts
  6. 9 16
      src/layouts/MainLayout.vue
  7. 0 6
      src/router/index.ts
  8. 11 2
      src/stores/auth.ts
  9. 91 15
      src/views/auth/Login.vue
  10. 69 3
      src/views/auth/OAuthCallback.vue
  11. 0 4
      vite.config.ts

+ 1 - 1
.env.dev

@@ -1,5 +1,5 @@
 # 开发环境配置
-VITE_APP_TITLE=管理平台 - 开发环境
+VITE_APP_TITLE=四川路桥样本中心 - 开发环境
 VITE_API_BASE_URL=http://192.168.92.61
 VITE_APP_ENV=dev
 VITE_APP_DEBUG=true

+ 1 - 1
.env.prod

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 应用配置
-VITE_APP_TITLE=SSO认证中心
+VITE_APP_TITLE=四川路桥样本中心
 VITE_APP_ENV=production
 VITE_APP_DEBUG=false
 

+ 1 - 1
index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8">
     <link rel="icon" href="/favicon.ico">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>四川路桥AI后台管理平台</title>
+    <title>四川路桥样本中心</title>
   </head>
   <body>
     <div id="app"></div>

+ 5 - 0
src/api/auth.ts

@@ -27,6 +27,11 @@ export const authApi = {
     return request.get('/api/v1/auth/userinfo')
   },
 
+  // 获取SSO授权URL
+  getSSOAuthorizeUrl(): Promise<ApiResponse<{ authorize_url: string }>> {
+    return request.get('/auth/sso/authorize')
+  },
+
   // 获取验证码
   getCaptcha(): Promise<ApiResponse<{ captcha_id: string; captcha_image: string }>> {
     return request.get('/api/v1/auth/captcha')

+ 11 - 7
src/api/request.ts

@@ -104,8 +104,10 @@ request.interceptors.response.use(
       }
 
       // 立即跳转
-      console.log('>>> 执行跳转: window.location.href = "/login"')
-      window.location.href = '/login'
+      const ssoLogoutUrl = localStorage.getItem('sso_logout_redirect_url')
+      const targetUrl = ssoLogoutUrl || '/login'
+      console.log('>>> 执行跳转: window.location.href =', targetUrl)
+      window.location.href = targetUrl
 
       // 阻止后续执行
       return new Promise(() => {})
@@ -166,19 +168,21 @@ request.interceptors.response.use(
       console.log('>>> 准备跳转到登录页')
       
       // 方式1: 立即使用window.location.href
+      const ssoLogoutUrl401 = localStorage.getItem('sso_logout_redirect_url')
+      const targetUrl401 = ssoLogoutUrl401 || '/login'
       console.log('>>> 方式1: window.location.href')
-      window.location.href = '/login'
-      
+      window.location.href = targetUrl401
+
       // 方式2: 备用 - 使用location.replace(会替换历史记录)
       setTimeout(() => {
         console.log('>>> 方式2: location.replace (备用)')
-        location.replace('/login')
+        location.replace(targetUrl401)
       }, 50)
-      
+
       // 方式3: 最后的保障 - 使用window.location.assign
       setTimeout(() => {
         console.log('>>> 方式3: window.location.assign (最后保障)')
-        window.location.assign('/login')
+        window.location.assign(targetUrl401)
       }, 100)
       
       console.log('>>> 401处理完成,已触发跳转')

+ 9 - 16
src/layouts/MainLayout.vue

@@ -4,7 +4,7 @@
       <!-- 头部 -->
       <el-header class="header">
         <div class="header-left">
-          <h1>四川路桥AI后台管理平台</h1>
+          <h1>四川路桥样本中心</h1>
         </div>
         <div class="header-right">
           <el-dropdown @command="handleCommand">
@@ -162,9 +162,6 @@ const activeMenu = computed(() => {
   if (path.startsWith('/admin')) {
     return path
   }
-  if (path.startsWith('/apps')) {
-    return '/apps'
-  }
   return path
 })
 
@@ -218,19 +215,11 @@ const getDefaultMenus = (): MenuItem[] => {
     },
     {
       id: 'profile',
-      name: 'profile', 
+      name: 'profile',
       title: '个人资料',
       path: '/profile',
       icon: 'User',
       children: []
-    },
-    {
-      id: 'apps',
-      name: 'apps',
-      title: '我的应用',
-      path: '/apps',
-      icon: 'Grid',
-      children: []
     }
   ]
   
@@ -336,10 +325,14 @@ const handleCommand = async (command: string) => {
           cancelButtonText: '取消',
           type: 'warning'
         })
-        
-        await authStore.logout()
+
+        const redirectUrl = await authStore.logout()
         ElMessage.success('已退出登录')
-        router.push('/login')
+        if (redirectUrl) {
+          window.location.href = redirectUrl
+        } else {
+          router.push('/login')
+        }
       } catch (error) {
         // 用户取消
       }

+ 0 - 6
src/router/index.ts

@@ -76,12 +76,6 @@ const routes: RouteRecordRaw[] = [
         component: () => import('@/views/admin/Permissions.vue'),
         meta: { requiresAdmin: true }
       },
-      {
-        path: 'admin/apps',
-        name: 'AdminApps',
-        component: () => import('@/views/admin/Apps.vue'),
-        meta: { requiresAdmin: true }
-      },
       {
         path: 'admin/logs',
         name: 'AdminLogs',

+ 11 - 2
src/stores/auth.ts

@@ -61,10 +61,12 @@ export const useAuthStore = defineStore('auth', () => {
   }
 
   // 登出
-  const logout = async (): Promise<void> => {
+  const logout = async (): Promise<string | null> => {
+    let redirectUrl: string | null = null
     try {
       if (token.value) {
-        await authApi.logout()
+        const response = await authApi.logout()
+        redirectUrl = response.data?.redirect_url || null
       }
     } catch (error) {
       console.error('登出失败:', error)
@@ -74,7 +76,14 @@ export const useAuthStore = defineStore('auth', () => {
       token.value = null
       userMenus.value = []
       removeToken()
+      // 保存 SSO 退出跳转地址供 401 拦截器使用
+      if (redirectUrl) {
+        localStorage.setItem('sso_logout_redirect_url', redirectUrl)
+      } else {
+        localStorage.removeItem('sso_logout_redirect_url')
+      }
     }
+    return redirectUrl
   }
 
   // 获取用户信息

+ 91 - 15
src/views/auth/Login.vue

@@ -2,11 +2,36 @@
   <div class="login-container">
     <div class="login-box">
       <div class="login-header">
-        <h1>四川路桥AI后台管理平台</h1>
+        <h1>四川路桥样本中心</h1>
         <p>请登录您的账户</p>
       </div>
-      
+
+      <!-- 单点登录 -->
+      <div class="sso-section">
+        <el-button
+          type="primary"
+          size="large"
+          class="login-button"
+          @click="handleSSOLogin"
+        >
+          <el-icon><Connection /></el-icon>
+          单点登录
+        </el-button>
+      </div>
+
+      <el-divider>
+        <span class="divider-text">或</span>
+      </el-divider>
+
+      <!-- 本地登录(管理员备用) -->
+      <div class="local-login-toggle">
+        <el-link type="info" @click="showLocalLogin = !showLocalLogin">
+          {{ showLocalLogin ? '收起本地登录' : '本地管理员登录' }}
+        </el-link>
+      </div>
+
       <el-form
+        v-show="showLocalLogin"
         ref="loginFormRef"
         :model="loginForm"
         :rules="loginRules"
@@ -22,7 +47,7 @@
             clearable
           />
         </el-form-item>
-        
+
         <el-form-item prop="password">
           <el-input
             v-model="loginForm.password"
@@ -35,7 +60,7 @@
             @keyup.enter="handleLogin"
           />
         </el-form-item>
-        
+
         <el-form-item v-if="showCaptcha" prop="captcha">
           <div class="captcha-container">
             <el-input
@@ -52,7 +77,7 @@
             />
           </div>
         </el-form-item>
-        
+
         <el-form-item>
           <div class="login-options">
             <el-checkbox v-model="loginForm.remember_me">
@@ -63,7 +88,7 @@
             </el-link>
           </div>
         </el-form-item>
-        
+
         <el-form-item>
           <el-button
             type="primary"
@@ -75,7 +100,7 @@
             登录
           </el-button>
         </el-form-item>
-        
+
         <el-form-item>
           <div class="register-link">
             还没有账户?
@@ -105,6 +130,7 @@ const loginFormRef = ref<FormInstance>()
 const loading = ref(false)
 const showCaptcha = ref(false)
 const captchaImage = ref('')
+const showLocalLogin = ref(false)
 
 const loginForm = reactive<LoginForm>({
   username: '',
@@ -126,6 +152,22 @@ const loginRules: FormRules = {
   ]
 }
 
+// 单点登录
+const handleSSOLogin = async () => {
+  try {
+    const response = await authApi.getSSOAuthorizeUrl()
+    console.log('[Login] getSSOAuthorizeUrl response:', response)
+    const authorizeUrl = response.data?.authorize_url || (response as any).authorize_url
+    if (authorizeUrl) {
+      window.location.href = authorizeUrl
+    } else {
+      console.error('获取SSO授权URL失败,未找到 authorize_url', response)
+    }
+  } catch (error) {
+    console.error('获取SSO授权URL失败:', error)
+  }
+}
+
 // 获取验证码
 const getCaptcha = async () => {
   try {
@@ -145,27 +187,27 @@ const refreshCaptcha = () => {
 // 处理登录
 const handleLogin = async () => {
   if (!loginFormRef.value) return
-  
+
   try {
     await loginFormRef.value.validate()
     loading.value = true
-    
+
     await authStore.login(loginForm)
-    
+
     ElMessage.success('登录成功')
-    
+
     // 重定向到原来要访问的页面或默认页面
     const redirect = route.query.redirect as string
     router.push(redirect || '/dashboard')
-    
+
   } catch (error: any) {
     console.error('登录失败:', error)
-    
+
     // 如果是验证码错误或需要验证码,显示验证码
     if (error.message?.includes('验证码') || !showCaptcha.value) {
       await getCaptcha()
     }
-    
+
     ElMessage.error(error.message || '登录失败')
   } finally {
     loading.value = false
@@ -173,9 +215,21 @@ const handleLogin = async () => {
 }
 
 onMounted(() => {
+  console.log('[Login] onMounted, query=', route.query, 'isAuthenticated=', authStore.isAuthenticated)
+
   // 如果已经登录,重定向到仪表盘
   if (authStore.isAuthenticated) {
     router.push('/dashboard')
+    return
+  }
+
+  // 如果 URL 带有 auto_sso=1,自动触发单点登录
+  const autoSso = route.query.auto_sso
+  if (autoSso === '1' || autoSso === 1 || (Array.isArray(autoSso) && autoSso[0] === '1')) {
+    console.log('[Login] 检测到 auto_sso=1,自动触发单点登录')
+    handleSSOLogin()
+  } else {
+    console.log('[Login] 未检测到 auto_sso 参数,显示登录页')
   }
 })
 </script>
@@ -214,6 +268,28 @@ onMounted(() => {
   font-size: 14px;
 }
 
+.sso-section {
+  margin-bottom: 10px;
+}
+
+.sso-section .el-button {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+
+.divider-text {
+  color: #999;
+  font-size: 12px;
+}
+
+.local-login-toggle {
+  text-align: center;
+  margin-bottom: 16px;
+}
+
 .login-form {
   width: 100%;
 }
@@ -249,4 +325,4 @@ onMounted(() => {
   color: #666;
   font-size: 14px;
 }
-</style>
+</style>

+ 69 - 3
src/views/auth/OAuthCallback.vue

@@ -1,12 +1,78 @@
 <template>
   <div class="callback-container">
     <el-card>
-      <h2>OAuth回调处理中...</h2>
-      <el-icon class="is-loading"><Loading /></el-icon>
+      <h2>{{ statusText }}</h2>
+      <el-icon class="is-loading" v-if="loading"><Loading /></el-icon>
     </el-card>
   </div>
 </template>
 
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+import { setToken } from '@/utils/auth'
+
+const router = useRouter()
+const route = useRoute()
+const authStore = useAuthStore()
+
+const loading = ref(true)
+const statusText = ref('登录处理中...')
+
+onMounted(async () => {
+  try {
+    const token = route.query.token as string
+    const refreshToken = route.query.refresh_token as string
+    const error = route.query.error as string
+    const errorDescription = route.query.error_description as string
+
+    if (error) {
+      statusText.value = `登录失败: ${errorDescription || error}`
+      ElMessage.error(statusText.value)
+      setTimeout(() => {
+        router.push('/login')
+      }, 2000)
+      return
+    }
+
+    if (!token) {
+      statusText.value = '登录失败: 缺少令牌'
+      ElMessage.error(statusText.value)
+      setTimeout(() => {
+        router.push('/login')
+      }, 2000)
+      return
+    }
+
+    // 存储token
+    setToken(token, refreshToken)
+    authStore.token = token
+
+    // 获取用户信息
+    await authStore.fetchUserInfo()
+
+    if (authStore.isAuthenticated) {
+      statusText.value = '登录成功,正在跳转...'
+      ElMessage.success('登录成功')
+      router.push('/dashboard')
+    } else {
+      throw new Error('获取用户信息失败')
+    }
+  } catch (err: any) {
+    console.error('OAuth回调处理失败:', err)
+    statusText.value = '登录失败,请重试'
+    ElMessage.error(err.message || '登录失败')
+    setTimeout(() => {
+      router.push('/login')
+    }, 2000)
+  } finally {
+    loading.value = false
+  }
+})
+</script>
+
 <style scoped>
 .callback-container {
   display: flex;
@@ -15,4 +81,4 @@
   min-height: 100vh;
   text-align: center;
 }
-</style>
+</style>

+ 0 - 4
vite.config.ts

@@ -29,10 +29,6 @@ export default defineConfig(({ command, mode }) => {
           target: env.VITE_API_BASE_URL || 'http://localhost:8000',
           changeOrigin: true,
         },
-        '/oauth': {
-          target: env.VITE_API_BASE_URL || 'http://localhost:8000',
-          changeOrigin: true,
-        },
       },
     },
     build: {