① 单点登录

单点登录是一种身份验证机制,允许用户通过一次登录即可访问多个相互信任的应用系统,无需在每个系统中重复登录。

核心原理是:一个中心的认证系统和多个信任该认证的子系统

使用独立登录系统实现单点登录

系统角色:

  1. 用户中心:独立的认证系统,负责用户身份验证、颁发令牌
  2. 业务系统(B项目):信任用户中心的子系统,通过验证令牌获取用户信息

核心流程:

  1. 用户访问B系统时,检查本地登录状态
  2. 未登录则跳转至用户中心登录页面
  3. 用户在用户中心完成认证
  4. 用户中心生成令牌并重定向回B系统
  5. B系统通过令牌获取用户信息,建立本地会话

核心实现步骤

  1. 从用户中心跳转到B项目的/userCenterLogin页面
  • 用户中心验证用户身份后,生成一次性token,重定向到B系统的回调地址:http://b-system.com/userCenterLogin?token=xxxxxxx
  1. 修改B项目的权限控制(permission.js)
  • 在路由守卫中添加SSO登录处理逻辑,主要修改点:
    1. 将/userCenterLogin加入白名单
    2. 处理从用户中心跳转回来的token验证
  1. 创建B项目的SSO回调页面:创建/userCenterLogin页面,专门处理从用户中心返回的token
  2. 实现B项目的store登录方法:添加skipLogin方法,处理SSO令牌验证
  3. 实现B项目的API接口:定义与用户中心通信的API接口

关键代码实现

  1. 路由守卫配置(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()
})
  1. 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>
  1. 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()
    })
  }
}
  1. 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 })
    })
  }
}
  1. 环境配置示例
// .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

总结

实现要点

  1. 中心化认证:所有登录请求统一由用户中心处理
  2. 令牌传递:通过URL参数传递一次性token
  3. 回调处理:每个子系统需要专门的回调页面处理SSO登录
  4. 会话同步:验证token后,在子系统建立本地会话
  5. 安全考虑:使用一次性token防止重放攻击
posted on 2020-08-27 11:06  pleaseAnswer  阅读(421)  评论(0)    收藏  举报