单点登录(SSO)前端(Vue2.X)改造
摘要
本文详细阐述了前端管理系统(manager-ui)从传统用户名密码登录方式向单点登录(SSO)方式的改造过程。文章将从原有登录机制、SSO实现方案、技术架构变更和核心代码实现等方面进行深入分析,为类似的系统改造提供技术参考。
1. 系统背景
1.1 项目概述
- 项目名称: manager-ui
- 技术栈: Vue.js 2.6.10 + Vue Router 3.0.2 + Vuex 3.1.0 + Element UI 2.13.2
- 架构模式: SPA (单页应用) + 前后端分离
- 改造目标: 实现企业级单点登录,提升用户体验,统一身份认证
1.2 改造动机
- 用户体验优化: 减少重复登录,实现一次登录全平台访问
- 安全性提升: 统一认证中心,集中权限管理
- 运维效率: 降低多系统维护成本
- 合规要求: 满足企业统一身份认证规范
2. 原有登录机制分析
2.1 传统登录流程
原有系统采用经典的用户名密码登录模式,具体流程如下:
2.1.0 传统登录时序图
2.1.1 登录组件结构
<!-- /src/views/login.vue -->
<template><div class="login"><el-form ref="loginForm" :model="loginForm" :rules="loginRules"><!-- 支持两种登录方式 --><div v-show="userRequired"><!-- 用户名密码登录 --><el-form-item prop="username"><el-input v-model="loginForm.username" placeholder="请输入用户名" /></el-form-item><el-form-item prop="password"><el-input v-model="loginForm.password" type="password" placeholder="请输入密码" /></el-form-item></div><div v-show="!userRequired"><!-- 手机验证码登录 --><el-form-item prop="mobile"><el-input v-model="loginForm.mobile" placeholder="请输入手机号" /></el-form-item><el-form-item prop="smsCode"><el-input v-model="loginForm.smsCode" placeholder="请输入验证码" /></el-form-item></div></el-form></div>
</template>
2.1.2 登录业务逻辑
// 登录处理函数
handleLogin: function() {this.$refs.loginForm.validate(valid => {if (valid) {if (this.changeLogins === '0') {// 用户名密码登录this.loading = truethis.$store.dispatch('Login', this.loginForm).then(() => {this.$router.push({ path: this.redirect || '/' })}).catch(() => {this.loading = false})} else {// 短信验证码登录this.$store.dispatch('smsLogin', this.loginForm).then(() => {this.$router.push({ path: this.redirect || '/' })})}}})
}
2.1.3 Vuex登录Actions
// /src/store/modules/user.js
actions: {// 传统登录Login({ commit }, userInfo) {let { username = '', password, verificationCode, wordKey } = userInfousername = username.trim()return new Promise((resolve, reject) => {login(username, password, verificationCode, wordKey).then(res => {setToken(res.data.token) // 存储Token到Cookiecommit('SET_TOKEN', res.data.token) // 更新Vuex状态resolve()}).catch(error => {reject(error)})})}
}
2.2 权限验证机制
2.2.1 路由守卫
// /src/permission.js
router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) {/* 已有token */if (to.path === '/login') {next({ path: '/' })} else {if (store.getters.roles.length === 0) {// 获取用户信息和权限store.dispatch('GetInfo').then(res => {const roles = res.data.rolesstore.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {router.addRoutes(accessRoutes) // 动态添加路由next({ ...to, replace: true })})})} else {next()}}} else {// 无token,跳转登录页next(`/login?redirect=${to.fullPath}`)}
})
2.3 传统方式的局限性
- 重复登录: 每个系统都需要单独登录
- 密码管理复杂: 用户需要记住多套账号密码
- 安全风险: 密码泄露风险分散到各个系统
- 运维成本: 每个系统独立维护用户数据
3. SSO改造方案设计
3.1 技术架构
3.1.1 SSO系统架构图
3.1.2 SSO交互架构图
3.1.3 核心组件设计
- SSO页面组件 (
/src/views/sso/index.vue
) - 路由配置更新 (
/src/router/index.js
) - 权限验证增强 (
/src/permission.js
) - 请求拦截优化 (
/src/utils/request.js
)
3.2 JWT Token机制
3.2.1 Token结构
// JWT Token 结构
{header: {"alg": "HS256","typ": "JWT"},payload: {"sub": "userId", // 用户ID"username": "username", // 用户名"roles": ["role1", "role2"], // 角色"permissions": ["perm1"], // 权限"exp": 1640995200, // 过期时间"iat": 1640908800 // 签发时间},signature: "signature"
}
4. SSO核心实现
4.1 SSO页面组件
4.1.1 组件模板
<!-- /src/views/sso/index.vue -->
<template><div class="sso-container"><div class="loading-wrapper"><!-- 加载状态 --><div v-if="loading" class="loading-content"><div class="loading-spinner"><i class="el-icon-loading"></i></div><p class="loading-text">正在验证登录信息,请稍候...</p></div><!-- 错误状态 --><div v-if="!loading && error" class="error-content"><div class="error-icon"><i class="el-icon-warning"></i></div><p class="error-message">{{ error }}</p><div class="error-actions"><el-button type="primary" @click="redirectToLogin">返回登录页面</el-button><el-button @click="redirectToPortal" v-if="showPortalButton">返回门户系统</el-button></div></div></div></div>
</template>
4.1.2 SSO业务逻辑
export default {name: 'SSO',data() {return {loading: true,error: null,token: null,showPortalButton: false}},created() {this.handleSSOLogin()},methods: {async handleSSOLogin() {try {console.log('开始SSO登录流程')// 1. 从URL参数获取token和重定向地址this.token = this.$route.query.tokenconst redirect = this.$route.query.redirect || '/index'if (!this.token) {throw new Error('缺少token参数,无法完成单点登录')}// 2. 验证token格式(JWT格式验证)if (!this.validateToken(this.token)) {throw new Error('token格式无效,请联系系统管理员')}// 3. 存储token到CookiesetToken(this.token)// 4. 更新Vuex状态this.$store.commit('SET_TOKEN', this.token)// 5. 获取用户信息await this.$store.dispatch('GetInfo')// 6. 生成动态路由const roles = this.$store.getters.rolesconst accessRoutes = await this.$store.dispatch('GenerateRoutes', { roles })// 7. 添加路由到路由器this.$router.addRoutes(accessRoutes)// 8. 标记为SSO登录sessionStorage.setItem('sso-login', 'true')// 9. 跳转到目标页面setTimeout(() => {this.$router.replace({ path: redirect })}, 1500)} catch (error) {console.error('SSO登录失败:', error)this.error = error.message || '单点登录失败,请重新登录'this.loading = falsethis.showPortalButton = true}},// JWT Token格式验证validateToken(token) {if (!token || typeof token !== 'string') {return false}const parts = token.split('.')if (parts.length !== 3) {return false}try {const payload = JSON.parse(atob(parts[1]))// 检查token是否过期if (payload.exp && payload.exp * 1000 < Date.now()) {return false}return true} catch (e) {return false}}}
}
4.2 路由配置更新
4.2.1 添加SSO路由
// /src/router/index.js
export const constantRoutes = [{path: '/login',component: () => import('@/views/login'),hidden: true},// SSO路由配置{path: '/sso',component: () => import('@/views/sso/index'),hidden: true,meta: { title: '单点登录' }}// ... 其他路由
]
4.3 权限验证增强
4.3.1 路由白名单更新
// /src/permission.js
// 添加SSO路径到白名单
const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/middleware', '/forgetPassword', '/sso']router.beforeEach((to, from, next) => {NProgress.start()if (getToken()) {// 已有token的处理逻辑保持不变// ...} else {// 没有tokenif (whiteList.indexOf(to.path) !== -1) {// 在免登录白名单,直接进入(包括SSO页面)next()} else {next(`/login?redirect=${to.fullPath}`)}}
})
4.4 请求拦截优化
4.4.1 Token失效处理
// /src/utils/request.js - 响应拦截器优化
service.interceptors.response.use(res => {if (!res.data) return const code = res.data.code || '000000'const message = errorCode[code] || res.data.msg || errorCode['default']if (code === '403000') {// Token失效处理const isFromSSO = sessionStorage.getItem('sso-login') === 'true'const isCurrentSSO = window.location.pathname === '/sso'// 避免在SSO页面重复弹框if (!isCurrentSSO) {MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录','系统提示',{confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning'}).then(() => {// 清除SSO标记,跳转到登录页sessionStorage.removeItem('sso-login')store.dispatch('LogOut').then(() => {location.href = '/login'})})}return Promise.reject(new Error(message))}// ... 其他错误处理
})
5. SSO跳转流程详解
5.1 SSO单点登录时序图
5.1.1 完整SSO登录流程时序图
5.1.2 SSO vs 传统登录对比时序图
-
前端Token验证机制
// SSO页面验证Token有效性 validateToken(token) {const parts = token.split('.')if (parts.length !== 3) return falsetry {const payload = JSON.parse(atob(parts[1]))// 检查过期时间if (payload.exp * 1000 < Date.now()) return falsereturn true} catch (e) {return false} }
-
状态同步机制
// 同步更新应用状态 setToken(this.token) // Cookie存储 this.$store.commit('SET_TOKEN', this.token) // Vuex状态 await this.$store.dispatch('GetInfo') // 获取用户信息
-
路由权限自动配置
// 动态生成并添加路由 const roles = this.$store.getters.roles const accessRoutes = await this.$store.dispatch('GenerateRoutes', { roles }) this.$router.addRoutes(accessRoutes)
5.2.2 跳转URL结构解析
http://10.1.73.208/sso?token=xxx&redirect=/index
│ │ │ │
│ │ │ └─ 登录成功后跳转的目标页面
│ │ └─ JWT Token(包含用户身份和权限信息)
│ └─ SSO处理页面
└─ 业务系统域名
参数说明:
token
: JWT格式的身份令牌,包含用户信息和权限redirect
: 登录成功后的跳转目标,默认为/index
5.2.3 自动登录成功的技术保障
- Token预验证:在跳转前确保Token有效性
- 状态预设置:提前配置好用户权限和路由
- 无缝切换:用户感知不到登录过程
- 错误兜底:Token无效时自动降级到传统登录
6. 安全性考量
6.1 Token安全机制
-
时效性控制
// Token过期时间检查 if (payload.exp && payload.exp * 1000 < Date.now()) {throw new Error('Token已过期') }
-
HTTPS传输
- 所有Token传输必须通过HTTPS加密
- 防止中间人攻击和Token泄露
-
跨域安全
// 验证Token来源域名 const allowedDomains = ['portal.company.com', 'auth.company.com']
6.2 防重放攻击
- Token唯一性:每次登录生成唯一Token
- 时间窗口:限制Token使用时间窗口
- 请求签名:关键操作增加请求签名验证
7. 性能优化
7.1 加载性能
-
路由懒加载
component: () => import('@/views/sso/index')
-
Token缓存策略
// 合理的Token缓存时间 setToken(token, { expires: 7 }) // 7天过期
7.2 用户体验优化
-
加载状态展示
<div v-if="loading" class="loading-content"><i class="el-icon-loading"></i><p>正在验证登录信息,请稍候...</p> </div>
-
错误处理友好化
catch (error) {this.error = error.message || '单点登录失败,请重新登录'this.showPortalButton = true // 提供返回选项 }
8. 测试与验证
8.1 功能测试用例
-
正常登录流程
- 验证Token有效时的自动登录
- 验证redirect参数的正确跳转
-
异常情况处理
- Token无效时的错误提示
- Token过期时的重新登录引导
-
权限验证
- 不同角色用户的页面访问权限
- 动态路由的正确生成
8.2 安全测试
- Token篡改测试
- 重放攻击测试
- 跨站脚本攻击(XSS)防护测试
9. 部署与运维
9.1 部署配置
-
Nginx配置
location /sso {try_files $uri $uri/ /index.html; }
-
环境变量配置
// 不同环境的SSO配置 const ssoConfig = {development: 'http://dev-auth.company.com',production: 'https://auth.company.com' }
9.2 监控告警
- SSO成功率监控
- Token失效率统计
- 登录异常告警
10. 总结与展望
10.1 改造效果
- 用户体验提升:实现了一次登录,全平台访问
- 安全性增强:统一身份认证,集中权限管理
- 维护成本降低:减少了重复的认证逻辑开发
10.2 技术收益
- 架构统一:建立了统一的前端认证标准
- 代码复用:SSO组件可在其他项目中复用
- 扩展性强:便于后续接入更多业务系统
10.3 后续优化方向
- 微前端集成:支持微前端架构下的SSO
- 移动端适配:扩展到移动端应用
- 多因子认证:增加短信、邮箱等多因子认证支持
10.4 最佳实践建议
- 统一标准:制定企业级SSO技术标准
- 安全第一:严格执行安全编码规范
- 用户体验:关注用户登录体验的每个细节
- 监控运维:建立完善的监控和告警机制