① 单点登录
单点登录是一种身份验证机制,允许用户通过一次登录即可访问多个相互信任的应用系统,无需在每个系统中重复登录。
核心原理是:一个中心的认证系统和多个信任该认证的子系统
使用独立登录系统实现单点登录
系统角色:
- 用户中心:独立的认证系统,负责用户身份验证、颁发令牌
- 业务系统(B项目):信任用户中心的子系统,通过验证令牌获取用户信息
核心流程:
- 用户访问B系统时,检查本地登录状态
- 未登录则跳转至用户中心登录页面
- 用户在用户中心完成认证
- 用户中心生成令牌并重定向回B系统
- B系统通过令牌获取用户信息,建立本地会话
核心实现步骤
- 从用户中心跳转到B项目的/userCenterLogin页面
- 用户中心验证用户身份后,生成一次性token,重定向到B系统的回调地址:
http://b-system.com/userCenterLogin?token=xxxxxxx
- 修改B项目的权限控制(permission.js)
- 在路由守卫中添加SSO登录处理逻辑,主要修改点:
- 将/userCenterLogin加入白名单
- 处理从用户中心跳转回来的token验证
- 创建B项目的SSO回调页面:创建/userCenterLogin页面,专门处理从用户中心返回的token
- 实现B项目的store登录方法:添加skipLogin方法,处理SSO令牌验证
- 实现B项目的API接口:定义与用户中心通信的API接口
关键代码实现
- 路由守卫配置(permission.js)
// 路由白名单列表。不用登陆也可以访问 -- 增加userCenterLogin
const whiteList = ['/login', '/userCenterLogin']
router.beforeEach(async(to, from, next) => {
NProgress.start()
document.title = getPageTitle(to.meta.title)
const hasToken = getToken()
if (hasToken) {
// 已登录状态
if (to.path === '/login') {
// 避免重复登录
next({ path: '/' })
NProgress.done()
}
// SSO回调页面特殊处理
else if (to.path === '/userCenterLogin') {
next()
NProgress.done()
}
// 检查用户信息是否已加载
else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
// 首次登录,获取用户信息...
}
}
}
}
// 未登录状态
else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
// 非白名单页面,跳转到登录页
// 实际应用中,这里可以跳转到用户中心
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
- SSO回调页面(userCenterLogin.vue)
<template>
<div class="sso-callback">
<div v-if="loading" class="loading">
<el-icon class="is-loading">
<loading />
</el-icon>
<p>正在登录中,请稍候...</p>
</div>
<div v-else-if="error" class="error">
<el-alert
title="登录失败"
type="error"
:description="error"
show-icon
:closable="false"
/>
<el-button type="primary" @click="retry">重试</el-button>
</div>
</div>
</template>
<script>
import { Loading } from '@element-plus/icons-vue'
export default {
name: 'UserCenterLogin',
components: { Loading },
data() {
return {
loading: true,
error: null
}
},
created() {
this.handleSSOLogin()
},
methods: {
handleSSOLogin() {
// 从URL中获取token
const urlParams = new URLSearchParams(window.location.search)
const token = urlParams.get('token')
if (!token) {
this.error = '未找到认证令牌'
this.loading = false
return
}
// 调用SSO登录接口
this.$store.dispatch('user/skipLogin', { ticket: token })
.then(() => {
// 登录成功,跳转到首页或重定向页面
const redirect = this.$route.query.redirect || '/'
this.$router.push(redirect)
})
.catch(error => {
console.error('SSO登录失败:', error)
this.error = error.message || '登录失败,请重试'
this.loading = false
})
},
retry() {
this.loading = true
this.error = null
this.handleSSOLogin()
}
}
}
</script>
- vuex store 中的sso登录方法
import { skipLogin, getInfo, logout } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
const state = {
token: getToken(),
name: '',
roles: []
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}
const actions = {
// SSO跳转登录
skipLogin({ commit }, { ticket }) {
return new Promise((resolve, reject) => {
skipLogin(ticket).then(response => {
const { data } = response
// 保存token到本地存储和vuex
commit('SET_TOKEN', data.token)
setToken(data.token)
// 获取用户信息
return store.dispatch('user/getInfo')
}).then(userInfo => {
resolve(userInfo)
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('验证失败,请重新登录')
}
const { roles, name } = data
// 验证返回的roles是否是非空数组
if (!roles || roles.length <= 0) {
reject('用户没有分配角色')
}
commit('SET_ROLES', roles)
commit('SET_NAME', name)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// 退出登录
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
// 调用用户中心的全局登出
window.location.href = `${process.env.VUE_APP_SSO_URL}/logout?service=${encodeURIComponent(window.location.origin)}`
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
}).catch(error => {
reject(error)
})
})
},
// 重置token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
})
}
}
- API接口定义
import request from '@/utils/request'
import qs from 'qs'
// 基础URL配置
const ssoApi = process.env.VUE_APP_SSO_API
export default {
// SSO跳转登录
skipLogin(ticket) {
return request({
url: `${ssoApi}/sso/login`,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: qs.stringify({ ticket })
})
},
// 获取用户信息
getInfo(token) {
return request({
url: `${ssoApi}/user/info`,
method: 'get',
params: { token }
})
},
// 全局登出
logout(token) {
return request({
url: `${ssoApi}/sso/logout`,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: qs.stringify({ token })
})
}
}
- 环境配置示例
// .env.development
VUE_APP_SSO_URL=http://sso-center.com
VUE_APP_SSO_API=http://sso-center.com/api
// .env.production
VUE_APP_SSO_URL=https://sso.yourdomain.com
VUE_APP_SSO_API=https://sso.yourdomain.com/api
总结
实现要点
- 中心化认证:所有登录请求统一由用户中心处理
- 令牌传递:通过URL参数传递一次性token
- 回调处理:每个子系统需要专门的回调页面处理SSO登录
- 会话同步:验证token后,在子系统建立本地会话
- 安全考虑:使用一次性token防止重放攻击
浙公网安备 33010602011771号