Vue实现单点登录

简介

什么是单点登录

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

SSO认证流程

image-20250207174327297

环境搭建

软件版本

名称 版本
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

验证

source ~/.zshrc
vue -V

项目配置

创建项目

vue create vue-sso

创建头部组件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 &copy;1999 mengxuegu.com &nbsp;All Rights Reserved&nbsp;
<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模块

npm install 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

npm install axios

创建src/.env.development

# 使用 VUE_APP_ 开头的变量会被 webpack 自动加载
# 定义请求的基础URL, 方便跨域请求时使用
VUE_APP_BASE_API = '/api'

# 接口服务地址, 以你自已的为主
VUE_APP_SERVICE_URL = 'https://mock.mengxuegu.com/mock/655c4c977c4edb526441554d/sso'

# cookie保存的域名,utils/cookie.js 要用
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_ 开头的变量会被 webpack 自动加载
# 定义请求的基础URL, 方便跨域请求时使用
VUE_APP_BASE_API = '/api'

# 接口服务地址, 以你自已的为主
VUE_APP_SERVICE_URL = 'https://mock.mengxuegu.com/mock/655c4c977c4edb526441554d/sso'


# cookie保存的域名,utils/cookie.js 要用
VUE_APP_COOKIE_DOMAIN = 'localhost'

创建vue.config.js

module.exports = {
devServer: {
port: 7000, // 端口号,如果端口号被占用,会自动提升1
host: "localhost", //主机名
https: false, //协议
open: true, //启动服务时自动打开浏览器访问
proxy: { // 开发环境代理配置
// '/dev-api': {
[process.env.VUE_APP_BASE_API] :{
// 目标服务器地址
target: process.env.VUE_APP_SERVICE_URL,
changeOrigin: true, // 开启代理服务器,
pathRewrite: {
// 将 请求地址前缀 /dev-api 替换为 空的,
// '^/dev-api': '',
[ '^' + process.env.VUE_APP_BASE_API]: ''
}
}
}
},

lintOnSave: false, // 关闭格式检查
productionSourceMap: false, // 打包时不会生成 .map 文件,加快打包速度

}

对接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的域设置

# cookie保存的域名,utils/cookie.js 要用
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.vueloginSubmit方法,触发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`, // 访问的是public/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>`
})
}
}
文章作者: 慕容峻才
文章链接: https://www.acaiblog.top/Vue实现单点登录/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿才的博客
微信打赏
支付宝打赏