简介
什么是单点登录
单点登录(Single Sign On),简称为SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
SSO认证流程

环境搭建
软件版本
名称 |
版本 |
node |
v20.9.0 |
npm |
10.1.0 |
vue/cli |
4.5.19 |
环境配置
设置npm仓库地址
npm config set registry http://localhost:8081/repository/npm/
|
设置npm全局安装模块目录
npm config set prefix '/Users/allen/workspace/node_modules'
|
安装vue-cli脚手架
npm cache clean --force npm install -g @vue/cli@4.5.19
|
配置环境变量,编辑~/zshrc
export NPM_HOME=/Users/allen/workspace/node_modules/ PATH=$PATH:$NPM_HOME/bin
|
验证
项目配置
创建项目
创建头部组件src/components/Layout/Header/index.vue
<template> <div class="mxg-header"> <div class="logo"> <a href="http://www.mengxuegu.com" title="梦学谷"> <img src="@/assets/image/logo.png" height="50px"/> </a> </div> </div> </template> <style scoped> .mxg-header{ width: 100%; height: 80px; border-top: 3px solid #345dc2; z-index: 10; } .logo{ width: 1200px; margin: 0 auto; //居中 overflow: hidden; margin-top: 15px; } </style>
|
创建底部组件src/components/Layout/Footer/index.vue
<template> <div class="mxg-footer"> <div class="footer-info"> CopyRight ©1999 mengxuegu.com All Rights Reserved <a href="https://www.beian.miit.gov.cn" target="_blank" rel="nofollow">陕ICP备2023006142号-1</a> </div> </div> </template> <style scoped> .mxg-footer{ width: 1200px; margin: 0 auto; line-height: 60px; border-top: 1px solid #ddd; } .footer-info{ text-align: center; font-size: 13px; color: #2c2c40 } .footer-info a{ color: #2c2c40; text-decoration: none; } </style>
|
创建布局组件src/components/Layout/index.vue
<template> <div> <app-header/> <div class="mxg-main"> <router-view/> </div> <app-footer/> </div> </template> <script> import AppHeader from '@/components/Layout/AppHeader' import AppFooter from '@/components/Layout/AppFooter' export default { components: { AppHeader, AppFooter } } </script> <style scoped> .mxg-main{ min-height: 650px; width: 100%; } </style>
|
路由渲染出口,编辑src/App.vue
<template> <div id="app"> <router-view/> </div> </template> <style scoped> body{ margin: 0; padding: 0px; font-family: "微软雅黑"; } </style>
|
编辑路由src/router/index.js
,引入布局
import Layout from "@/components/Layout/index.vue";
const routes = [ { path: '/', name: 'Home', component: Layout } ]
|
登录与注册组件
创建src/views/auth/login.vue
登录组件,参考: login.vue
路由配置
安装Vue-Router模块
编辑src/router/index.js
import { createRouter, createWebHistory } from 'vue-router' import Layout from "@/components/Layout/index.vue"; import Login from "@/views/auth/login.vue";
const routes = [ { path: '/', component: Layout, children: [ { path: '', component: Login } ] } ]
const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes })
export default router
|
编辑src/main.js
将路由对象添加到Vue实例中
import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store'
createApp(App).use(store).use(router).mount('#app')
|
封装Axios
安装axios
创建src/.env.development
VUE_APP_BASE_API = '/api'
VUE_APP_SERVICE_URL = 'https://mock.mengxuegu.com/mock/655c4c977c4edb526441554d/sso'
VUE_APP_COOKIE_DOMAIN = 'localhost'
|
创建src/utils/request.js
import axios from "axios";
const service = axios.create({ baseURL: process.env.VUE_APP_SERVICE_URL, timeout: 10000 })
service.interceptors.request.use( config => { return config }, error => { return Promise.reject(error) } )
service.interceptors.response.use( response => { const res = response.data return res }, error => { return Promise.reject(error) } )
export default service
|
创建.env.development
VUE_APP_BASE_API = '/api'
VUE_APP_SERVICE_URL = 'https://mock.mengxuegu.com/mock/655c4c977c4edb526441554d/sso'
VUE_APP_COOKIE_DOMAIN = 'localhost'
|
创建vue.config.js
module.exports = { devServer: { port: 7000, host: "localhost", https: false, open: true, proxy: { [process.env.VUE_APP_BASE_API] :{ target: process.env.VUE_APP_SERVICE_URL, changeOrigin: true, pathRewrite: { [ '^' + process.env.VUE_APP_BASE_API]: '' } } } },
lintOnSave: false, productionSourceMap: false,
}
|
对接Mock.js模拟数据接口
创建接口
URL |
Method |
描述 |
/auth/login |
post |
登录接口 |
mock.js语法
{ "code": 20000, "message": "登录成功", "data": { "access_token": "@word(30)", "token_type": "bearer", "refresh_token": "@word(30)", "expires_in": "@natural", "scope": "all", "userInfo": { "uid": "@natural", "username": "@name", "mobile": /^(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/, "email": "@email", "nickName": "@cname", "imageUrl": "https://acaiblog.oss-cn-hangzhou.aliyuncs.com/userImage.jpeg" }, "jti": "@word(20)" } }
|
实现登录功能
创建登录认证接口
创建src/api/auth.js
import request from "@/utils/request";
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
const auth = { username: 'acai', password: '123' }
export function login(data){ return request({ headers, auth, url: `/auth/login`, method: "post", params: data }) }
|
Vux状态信息管理
当登录成功后,后台响应的 userInfo、access_token、refresh_token 信息使用 Vuex 进行管理,并且将这些信息 保存到浏览器 Cookie 中。
安装 js-cookie 和 vuex 模块
npm install js-cookie vuex
|
创建src/utils/cookie.js
,具体内容参考:cookie.js
创建认证模块文件src/store/modules/auth.js
,添加对userInfo,accessToken,refreshToken状态管理。具体内容参考:auth.js
查看.env.development
cookie的域设置
VUE_APP_COOKIE_DOMAIN = 'localhost'
|
在src/store/index.js
创建Vuex实例,导入modules/auth.js状态模块
import { createStore } from 'vuex' import auth from './modules/auth'
export default createStore({ state: { }, mutations: { }, actions: { }, modules: { auth } })
|
检查src/main.js
是否已经将store导入到Vue实例
import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store'
createApp(App).use(store).use(router).mount('#app')
|
提交登录出发action
在登录页面的created钩子函数获取redirectURL,如果登录成功重定向到redirectURL。编辑src/views/auth/login.vue
created() { if(this.$route.query.redirectURL){ this.redirectURL = this.$route.query.redirectURL } }
|
修改src/views/auth/login.vue
的loginSubmit
方法,触发store/modules/auth.js
中的UserLogin
进行登录。并导入@/utils/validate
正则表达式校验用户名是否合法。
<script > import { isvalidUsername } from "@/utils/validate";
export default { methods: { loginSubmit() { if(this.subState){ return false } if(!isvalidUsername(this.loginData.username)){ this.loginMessage = '用户名不正确' return false } if(this.loginData.password.length <6){ this.loginMessage = '用户密码不小于6个字符串' return false } this.subState = true this.$store.dispatch('UserLogin', this.loginData).then(response => { const { code, message } = response if(code === 20000) { window.location.href = this.redirectURL }else { this.loginMessage = message } this.subState = false }).catch(error => { this.subState = false this.loginMessage = error })
}, }, } </script>
|
测试
登录成功后,重定向回到redirectURL参数值对应的页面,如果不带 redirectURL 重写向到 mengxuegu.com
注册功能实现
创建mock.js接口
创建查询用户接口
URL |
method |
描述 |
mock.js语法 |
/system/api/username/{username} |
get |
查询用户 |
{"code":20000,"message":"查询成功","data":true} |
创建用户注册接口
URL |
method |
描述 |
mock.js语法 |
system/api/user/register |
post |
用户注册 |
{"code":20000,"message":"注册成功"} |
定义API调用用户协议和注册接口
编辑src/api/auth.js
export function getProtocol(){ return request({ url: `${window.location.href}/protocol.html`, method: 'get' }) }
export function getUserByUsername(username){ return request({ url: `/system/api/user/username/{username}`, method: 'get' }) }
export function register(data){ return request({ headers, auth, url: `/system/api/user/register`, method: 'post', params: data }) }
|
提交注册数据
编辑src/views/auth/login.vue
在create钩子函数添加获取协议内容
<script > import { getProtocol, getUserByUsername, register } from "@/api/auth";
export default { async created() { if(this.$route.query.redirectURL){ this.redirectURL = this.$route.query.redirectURL } // 获取协议内容 this.xieyiContent = await getProtocol() }, } </script>
|
编辑src/views/auth/login.vue
在regSubmit方法中提交注册表单数据
<script > import { isvalidUsername } from "@/utils/validate"; import { getProtocol, getUserByUsername, register } from "@/api/auth";
export default { methods: {
// 提交注册 async regSubmit() { // 如果在注册中不允许登录 if(this.subState){ return false } if(!isvalidUsername(this.registerData.username)){ this.regMessage = "请输入4-30位用户名, 中文、数字、字母和下划线" return false } // 校验用户名是否存在 const { code, message, data } = await getUserByUsername(this.registerData.username) if(code !== 20000){ this.regMessage = message } // data为true已被注册,fase未被注册 if(data) { this.regMessage = "用户名已被注册,请重新输入用户名" return false } if(this.registerData.password.length < 6 || this.registerData.password.length > 30){ this.regMessage = '请输入6-30位密码,区分大小写且不可有空格' return false } if(this.registerData.password !== this.registerData.repassword){ this.regMessage = '两次输入密码不一致' return false } if(!this.registerData.check){ this.regMessage = '请阅读并同意用户协议' return false } this.subState = true //提交中 // 提交注册 register(this.registerData).then(response =>{ this.subState = false const { code, message } = response if(code === 20000){ // 登录成功切换登录页面 this.changetab(1) }else { this.regMessage = message } }).catch(error =>{ this.subState = false this.regMessage = message }) }
}, } </script>
|
退出单点登录系统
退出登录流程
应用系统退出,全部发送请求到当前认证中心进行处理,发送请求后台删除用户登录数据,并将 cookie 中的用户数据清除。

创建mockjs接口
URL |
method |
描述 |
MockJs语法 |
/auth/logout |
get |
退出登录 |
{"code":20000,"message":"退出登录成功"} |
定义退出登录API接口
编辑src/api/auth.js
export function logout(accessToken){ return request({ url: `auth/logout`, method: 'get', params: { accessToken } }) }
|
定义Vuex退出action
编辑src/store/modules/logout.js
import { logout } from "@/api/auth";
const actions = { UserLogout({ state, commit }, redirectURL){ logout(state.accessToken).then(() =>{ commit('RESET_USER_STATE') window.location.href = redirectURL || '/' }).catch(() =>{ commit('RESET_USER_STATE') window.location.href = redirectURL || '/' }) } } export default { actions }
|
路由拦截器退出操作
应用系统访问 http://localhost:7000/logout?redirectURL=xxx 进行退出,我们添加路由前置拦截 /logout 路由请求进行调用 UserLogout 进行退出操作。编辑src/router/index.js
import store from "@/store"; router.beforeEach((to, form, next) =>{ if(to.path === '/logout'){ store.dispatch('UserLogout', to.query.redirectURL) }else { next() } })
|
刷新令牌
刷新令牌流程
当应用系统请求后台资源接口时,要在请求头带上 accessToken 去请求接口,如果 accessToken 有效,资源服务器正常响应数据。
如果访问令牌 accessToken 过期,资源服务器会响应 401 状态码 。当应用系统接收到 401 状态码时,通过刷新令牌 refreshToken 获取去请求新令牌完成新的重新身份。

创建刷新令牌组件
编辑src/views/auth/logout.vue
具体参考: refresh.vue
添加刷新令牌路由
编辑src/router/index.js
{ path: '/refresh', component: Layout, children: [ { path: '', component: ()=> import('@/views/auth/refresh.vue') } ] }
|
添加刷新令牌MockJS接口
URL |
method |
描述 |
/auth/user/refreshToken |
get |
刷新令牌 |
mockjs语法
{ "code": 20000, "message": "登录成功", "data": { "access_token": "@word(30)", "token_type": "bearer", "refresh_token": "@word(30)", "expires_in": "@natural", "scope": "all", "userInfo": { "uid": "@natural", "username": "@name", "mobile": /^(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/, "email": "@email", "nickName": "@cname", "imageUrl": "https://acaiblog.oss-cn-hangzhou.aliyuncs.com/userImage.jpeg" }, "jti": "@word(20)" } }
|
定义刷新令牌API接口
编辑src/api/auth.js
export function refreshToken(refreshToken){ return request({ headers, auth, url: `/auth/user/refreshToken`, params: { refreshToken } }) }
|
Vuex发送请求与重置状态
编辑src/store/modules/login.js
import { refreshToken } from "@/api/auth";
const actions = { SendRefreshToken({state, commit}){ return new Promise((resolve, reject) =>{ if(!state.refreshToken){ commit('RESET_USER_STATE') reject('没有令牌') return } refreshToken(state.refreshToken).then(response =>{ commit('SET_USER_STATE', state.refreshToken) resolve() }).catch(error =>{ commit('RESET_USER_STATE') reject(error) }) }) } }
|
重构刷新令牌组件
在src/views/auth/refresh.vue
中的refreshLogin
方法中触发store/modules/auth.js
中的SendRefreshToken
行为来完成刷新身份。
methods: { refreshLogout(){ this.$store.dispatch('SendRefreshToken').then(response =>{ window.location.href = this.redirectURL }).catch(error =>{ this.message = `您的身份已过期,请点击<a href="/?redirectURL${this.redirectURL}">重新登录<a>` }) } }
|