Vue-element-admin 是一个基于 Vue.js 和 Element UI 的后台管理系统框架,提供了丰富的组件和功能,可以帮助开发者快速搭建现代化的后台管理系统。
一、基本知识
(一)Vue-element-admin 的主要文件和目录
vue-element-admin/
|-- build/ # 构建相关配置文件
| |-- build.js # 生产环境构建脚本
| |-- check-versions.js # 检查 Node.js 和 npm 版本的脚本
| |-- logo.png # 构建 Logo
| |-- utils.js # 构建工具函数
| |-- vue-loader.conf.js # Vue loader 配置
| |-- webpack.base.conf.js # webpack 基础配置
| |-- webpack.dev.conf.js # webpack 开发环境配置
| |-- webpack.prod.conf.js # webpack 生产环境配置
|
|-- config/ # 项目配置
| |-- dev.env.js # 开发环境变量配置
| |-- index.js # 项目配置文件
| |-- prod.env.js # 生产环境变量配置
|
|-- src/ # 源代码
| |-- api/ # 接口请求相关
| |-- assets/ # 静态资源
| |-- components/ # 全局公用组件
| |-- directive/ # 自定义指令
| |-- icons/ # 图标
| |-- layout/ # 全局布局
| |-- router/ # 路由配置
| |-- store/ # 全局状态管理
| |-- styles/ # 全局样式
| |-- utils/ # 工具函数
| |-- views/ # 页面组件
| |-- App.vue # 入口页面
| |-- main.js # 入口 JS 文件| |-- permission.js # 路由守卫 文件
|
|-- static/ # 静态资源
|
|-- .babelrc # Babel 配置
|-- .editorconfig # 编辑器配置
|-- .eslintignore # ESLint 忽略文件配置
|-- .eslintrc.js # ESLint 配置
|-- .gitignore # Git 忽略文件配置
|-- index.html # 入口 HTML 文件
|-- package.json # 项目信息和依赖配置
|-- README.md # 项目说明文档
|-- vue.config.js # Vue CLI 配置
(二)单点登录系统涉及的文件和概念
- 客户端ID和客户端密钥:前端应用和后端应用在与单点登录系统通信时,需要提供客户端ID和客户端密钥进行身份验证。
- 授权码(authorization code):单点登录系统验证用户身份成功后生成的一次性授权码,用于换取访问令牌和刷新令牌。
- 访问令牌(access token):单点登录系统验证成功后返回给前端应用的令牌,用于后续请求时进行身份验证。
- 刷新令牌(refresh token):单点登录系统验证成功后返回给前端应用的令牌,用于在访问令牌过期时更新访问令牌。
- 前端应用使用的SDK或库文件:前端应用需要集成相应的SDK或库文件以便与单点登录系统进行通信。
- 单点登录系统的API文档:开发人员需要根据API文档了解单点登录系统提供的接口和参数,以便正确调用API接口。
二、 前端实现单点登录的基本流程
设计思路:为了避免code和access_token的泄露,所以大部分的和单点登录系统(统一认证)的交互都放到后端进行,前端尽可能的复用原来的代码,进行小的改动。整体思路如下:
1、登录页面--login.vue(修改原login.vue):用户访问前端应用,前端应用将用户重定向到后端登录接口。
<template>
<div class="login-container">
<div>正在重定向到登录页面...</div>
</div>
</template>
<script>
export default {
created() {
const baseUrl = process.env.VUE_APP_BASE_API;
//重定向到后端的SSOlogin/login接口
window.location.href = `${baseUrl}/SSOlogin/login`;
},
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
}
</style>
2、 响应页面--callback.vue(新建):检查后端返回的URL+token访问链接中是否存在后端返回的token,如果存在则调用登录函数user/login进行前端的登录操作(调用login函数进行前端的登录,目的是将token持久化和存储到VUEX中,尽可能少的改动原来的代码。),并根据登录结果进行不同的处理。如果没有获取到token,则输出错误信息并终止后续逻辑。
<template>
<div>
<h1>Loading...</h1>
</div>
</template>
<script>
export default {
name: "Callback",
data() {
return {
};
},
created() {
// 获取 URL 查询参数中的授权码
const token = this.$route.query.token;
// 利用后端传递的token,调用login函数进行前端的登录,目的是将token持久化和存储到VUEX中,尽可能少的改动原来的代码
if (token) {
this.loading = true;
this.$store.dispatch("user/login",token)
.then(() => {
// 登录成功,路由跳转
this.$router.push({ path: this.redirect || "/" });
this.loading = false;
})
.catch(() => {
this.loading = false;
});
} else {
console.log("error submit!!");
return false;
}
},
};
</script>
对应的store/user.js。
3、 数据仓库 store/user.js,修改一下原来的Login函数即可。
原Login函数为:
//这里在处理登录业务
async login({ commit }, userInfo) {
//解构出用户名与密码
const { username, password } = userInfo;
let result = await login({ username: username.trim(), password: password });
if(result.code==200){
//vuex存储token
commit('SET_TOKEN',result.data.token);
//本地持久化存储token
setToken(result.data.token);
return 'ok';
}else{
return Promise.reject(new Error('faile'));
}
},
修改后的Login为:
// 登录 action
async login({ commit }, token) {
try {
// 设置 token 持久化
commit('SET_TOKEN', token);
// 将 token 值保存在浏览器的本地存储中
setToken(token);
// 可以根据需要返回其他数据或状态
return "ok";
} catch (error) {
// 异常时返回一个被拒绝的 Promise 对象
return Promise.reject(error);
}
},
所以,修改login函数,去掉了使用用户名和密码发送请求到后端接口验证登录,完整的代码如下:
// 引入需要使用的函数和模块
import { getInfo, logout } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
// 定义获取默认状态的函数
const getDefaultState = () => {
return {
token: getToken(), // 使用 getToken 函数获取 token 值
name: '',
avatar: ''
}
}
// 定义 Vuex 模块的状态
const state = getDefaultState()
// 定义 Vuex 模块的变更操作
const mutations = {
// 重置状态为默认状态
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
// 设置 token
SET_TOKEN: (state, token) => {
state.token = token
},
// 设置用户名
SET_NAME: (state, name) => {
state.name = name
},
// 设置用户头像
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
}
}
// 定义 Vuex 模块的异步操作
const actions = {
// 用户登录
// 登录 action
async login({ commit }, token) {
try {
// 设置 token
commit('SET_TOKEN', token);
// 将 token 值保存在浏览器的本地存储中
setToken(token);
// 可以根据需要返回其他数据或状态
return "ok";
} catch (error) {
// 异常时返回一个被拒绝的 Promise 对象
return Promise.reject(error);
}
},
async getInfo({ commit, state }) {
try {
// 发送请求获取用户信息
const response = await getInfo(state.token)
// 如果响应数据不存在,说明验证失败
if (!response.data) {
throw new Error('验证失败,请重新登录。')
}
// 获取用户名和头像
const { name, avatar } = response.data
// 设置用户名
commit('SET_NAME', name)
// 设置用户头像
commit('SET_AVATAR', avatar)
// 返回完整的响应对象
return response
} catch (error) {
return Promise.reject(error)
}
},
// 用户注销
async logout({ commit, state }) {
try {
// 发送请求注销用户登录状态
await logout(state.token)
// 从本地存储中删除 token
removeToken()
// 重置路由
resetRouter()
// 重置状态
commit('RESET_STATE')
} catch (error) {
return Promise.reject(error)
}
},
// 重置 token 值
async resetToken({ commit }) {
try {
// 从本地存储中删除 token
removeToken()
// 重置状态
commit('RESET_STATE')
} catch (error) {
return Promise.reject(error)
}
}
}
// 导出 Vuex 模块
export default {
namespaced: true, // 开启命名空间
state,
mutations,
actions
}
4、 数据仓库store/user.js引入的函数和模块
为了更清晰的了解整个过程,将上面store/user.js引入的函数和模块也贴在下面即:
因为 store/user.js中不用再向后端发请求,所以api/user.js中就可以把login删掉了。
api/user.js
//api/user.js
import request from '@/api/request/request'
export function getInfo(token) {
return request({
url: '/user/getInfo',
method: 'get',
params: { token }
})
}
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
}
utils/auth.js:用于处理用户身份验证 token 的工具函数,主要涉及对浏览器 Cookie 的操作。getToken:获取用户的身份验证 token,setToken:设置用户的身份验证 token,removeToken:移除用户的身份验证 token。
//utils/auth.js
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}
5、路由守卫--permission.js:(修改原来的)在每次路由导航之前进行身份验证和权限控制,确保用户在正确的身份状态下访问页面,并在页面切换后完成进度条的展示。
如果用户已登录且获取了用户信息,则直接跳转到目标页面;
如果用户未登录或获取用户信息失败,则跳转至登录页。
同时,白名单内的页面可以在未登录状态下直接访问。
修改:配置白名单,把callback加进去
注意:通过 user/getInfo 获取用户信息 await store.dispatch('user/getInfo'),所以api/user.js 里面要有getInfo函数。
permission.js 完整的代码如下:
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // 引入进度条库
import 'nprogress/nprogress.css' // 引入进度条样式
import { getToken } from '@/utils/auth' // 引入获取 token 的方法
import getPageTitle from '@/utils/get-page-title' // 引入获取页面标题的方法
NProgress.configure({ showSpinner: false }) // 配置进度条
const whiteList = ['/login','/callback'] // 定义无需登录即可访问的白名单路由
router.beforeEach(async(to, from, next) => {
NProgress.start() // 开始进度条
document.title = getPageTitle(to.meta.title) // 设置页面标题
const hasToken = getToken() // 获取 token
if (hasToken) {
if (to.path === '/login') {
// 如果已登录却访问登录页,则重定向到首页
next({ path: '/' })
NProgress.done() // 完成进度条
} else {
const hasGetUserInfo = store.getters.name // 判断是否已获取用户信息
if (hasGetUserInfo) {
// 如果已获取,则直接进入路由
next()
} else {
try {
// 如果未获取,则通过 user/getInfo 获取用户信息
await store.dispatch('user/getInfo')
console.log("获取用户信息")
next()
} catch (error) {
// 如果获取用户信息失败,则重置 token 并跳转至登录页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
// window.location.href = `www.baidu.com`;
NProgress.done()
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
// 如果在白名单中,则直接进入路由
next()
} else {
// 否则跳转至登录页
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done() // 完成进度条
})
6、请求拦截器中添加token到请求头:(和原来一样)在请求拦截器中,从cookie中获取token,并将其添加到请求的头信息中,这样可以确保每次请求都带上了token。
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建一个axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // 接口的基础路径
timeout: 5000 // 请求超时时间
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 在请求发送前做一些处理
if (store.getters.token) {
// 如果有token就在请求头中加上token
config.headers['token'] = getToken()
}
return config
},
error => {
// 对请求错误做些什么
console.log(error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
// 对响应数据做一些处理,这里只返回响应数据中的data部分
const res = response.data
// 如果自定义的响应码不是20000,就判断为错误
if (res.code !== 20000 && res.code !== 200) {
// 在页面上显示错误信息
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// 重新登录
MessageBox.confirm('您已经登出,您可以取消以留在此页面,或重新登录', '确认登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
// 返回一个被拒绝的Promise对象,用来表示错误
return Promise.reject(new Error(res.message || 'Error'))
} else {
// 如果没有错误,就返回响应数据中的data部分
return res
}
},
error => {
// 对响应错误做些什么
console.log('err' + error)
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
7、获取用户信息并渲染页面:在需要展示用户信息的地方,从cookie中获取用户信息,并将其渲染到页面上。