Vue的权限管理
目的:完成不同用户 登录系统 获取不同的权限,权限体现在菜单栏可见的菜单
核心技术栈:vue路由守卫
、addRoute
、vuex
、axios
标注颜色的部分 会 对应 颜色的引用
Step1:规划整体权限分布
规划整体的权限分布,这里可以是各个权限拥有的权限,比如
管理员:首页、团队管理、团队新闻、知识分享
普通者:首页、团队新闻、知识分享
Step2:定义路由
关于初始化路由
src
|--router
|--index.js
// 初始化路由
const routes = [
{
path: '/login',
name: 'Login',
component: Login
},
]
// 动态路由
const DynamicRoutes = [
{
path: "/",
component: () => import('../views/Main.vue'),
redirect: "index",
meta: {
requiresAuth: true,
},
children: []
},
{
path: "*",
component: () => import("../views/404.vue"),
},
{
path: "/403",
component: () => import("../views/403.vue"),
}
]
关于所有路由信息
存储所有前端需要的路由信息,用于与后端传来的权限路由进行筛选(utils中进行定义筛选文件)
src
|--router
|--index.js
|--dynamic-routes.js
//这里放动态路由的信息
const Fashion = () => import('../views/Fashion.vue')
const News = () => import('../views/News.vue')
const Share = () => import('../views/Share.vue')
const Manage = () => import('../views/Manage.vue')
const Cloud = () => import('../views/Cloud.vue')
const Index = () => import("../views/Index.vue")
export const mydynamicRoutes = [
{
path: "index",
component: Index,
meta: {
name: "首页",
icon: "el-icon-s-home",
}
},
{
path: "Fashion",
component: Fashion,
meta: {
name: "个人风采",
icon: "el-icon-picture-outline-round",
}
},
{
path: "News",
component: News,
meta: {
name: "团队新闻",
icon: "el-icon-s-opportunity",
}
},
{
path: "News/page",
component: ,
meta: {
name: "新闻页面",
}
},
{
path: "Share",
component: Share,
meta: {
name: "知识分享",
icon: "el-icon-s-promotion",
}
},
{
path: "Share/page",
component: () => import('../components/shareItem.vue'),
meta: {
name: "分享页面",
}
},
{
path: "Manage",
component: Manage,
meta: {
name: "团队管理",
icon: "el-icon-user-solid",
}
},
{
path: "Cloud",
component: Cloud,
meta: {
name: "本地的云",
icon: "el-icon-partly-cloudy",
}
},
]
关于路由守卫
import router from "./index";
import store from "@/store/index";
//第一层if-else(判断用户是否获取到token)
//1.当用户未登录时,只能访问login页面 与 不需要权限就能访问的页面
//第二层if-else(判断未登录的用户是否有权访问想去的页面)
//1.to.matched.length > 0表示当前要去的路由是存在的,也就是能matched到,且matched到的路由也不需要auth权限
//这里有个小tip就是to.matched它会将嵌套了n层的路由扁平化,所以to.matched.some(record => record.meta.requiresAuth)
//可以做到获取嵌套路由是否需要权限
//2.如果未匹配到路由/需要权限才能访问,那就跳转到login
//2.当用户登陆后,获取到token才能访问 对应权限的页面
//第二层if-else(判断用户是否获取到权限列表)
//1.未获取到权限列表,去获取完权限再回来进行路由跳转
//2.获取到权限列表,不是login页面就放行到用户要去的页面,是login那就停在原来的页面
router.beforeEach((to, from, next) => {
if (!store.state.defaultState.UserToken) {
if (to.matched.length > 0 && !to.matched.some(record => record.meta.requiresAuth)) {
next()
}
else {
next({
path: "/login"
})
}
} else {
console.log("store.state.permission.permissionList before:", store.state.permission.permissionList);
if (!store.state.permission.permissionList) {
store.dispatch("permission/FETCH_PERMISSION").then(() => {
router.push({ path: to.path });
})
}
else {
if (to.path !== "/login") {
next()
} else {
next(from.fullPath)
}
}
}
})
注释:
//第一层if-else(判断用户是否获取到token)
//1.当用户未登录时,只能访问login页面 与 不需要权限就能访问的页面
//第二层if-else(判断未登录的用户是否有权访问想去的页面)
//1.to.matched.length > 0表示当前要去的路由是存在的,也就是能matched到,且matched到的路由也不需要auth权限
//这里有个小tip就是to.matched它会将嵌套了n层的路由扁平化,所以to.matched.some(record => record.meta.requiresAuth)
//可以做到获取嵌套路由是否需要权限
//2.如果未匹配到路由/需要权限才能访问,那就跳转到login
//2.当用户登陆后,获取到token才能访问 对应权限的页面
//第二层if-else(判断用户是否获取到权限列表)
//1.未获取到权限列表,去获取完权限再回来进行路由跳转
//2.获取到权限列表,不是login页面就放行到用户要去的页面,是login那就停在原来的页面
import router from "./index";
import store from "@/store/index";
//第一层if-else(判断用户是否获取到token)
//1.当用户未登录时,只能访问login页面 与 不需要权限就能访问的页面
//第二层if-else(判断未登录的用户是否有权访问想去的页面)
//1.to.matched.length > 0表示当前要去的路由是存在的,也就是能matched到,且matched到的路由也不需要auth权限
//这里有个小tip就是to.matched它会将嵌套了n层的路由扁平化,所以to.matched.some(record => record.meta.requiresAuth)
//可以做到获取嵌套路由是否需要权限
//2.如果未匹配到路由/需要权限才能访问,那就跳转到login
//2.当用户登陆后,获取到token才能访问 对应权限的页面
//第二层if-else(判断用户是否获取到权限列表)
//1.未获取到权限列表,去获取完权限再回来进行路由跳转
//2.获取到权限列表,不是login页面就放行到用户要去的页面,是login那就停在原来的页面
router.beforeEach((to, from, next) => {
if (!store.state.defaultState.UserToken) {
if (to.matched.length > 0 && !to.matched.some(record => record.meta.requiresAuth)) {
next()
}
else {
next({
path: "/login"
})
}
} else {
console.log("store.state.permission.permissionList before:", store.state.permission.permissionList);
if (!store.state.permission.permissionList) {
store.dispatch("permission/FETCH_PERMISSION").then(() => {
router.push({ path: to.path });
})
}
else {
if (to.path !== "/login") {
next()
} else {
next(from.fullPath)
}
}
}
})
注释:
//第一层if-else(判断用户是否获取到token)
//1.当用户未登录时,只能访问login页面 与 不需要权限就能访问的页面
//第二层if-else(判断未登录的用户是否有权访问想去的页面)
//1.to.matched.length > 0表示当前要去的路由是存在的,也就是能matched到,且matched到的路由也不需要auth权限
//这里有个小tip就是to.matched它会将嵌套了n层的路由扁平化,所以to.matched.some(record => record.meta.requiresAuth)
//可以做到获取嵌套路由是否需要权限
//2.如果未匹配到路由/需要权限才能访问,那就跳转到login
//2.当用户登陆后,获取到token才能访问 对应权限的页面
//第二层if-else(判断用户是否获取到权限列表)
//1.未获取到权限列表,去获取完权限再回来进行路由跳转
//2.获取到权限列表,不是login页面就放行到用户要去的页面,是login那就停在原来的页面
Step3:配置网络接口
自定义封装axios,加入拦截器、封装get和post请求
关于拦截器
请求拦截器用于 请求头的权限验证添加
响应拦截器用于 处理错误信息,获取后端返回数据的进一步处理
下图为axios默认response返回的结果
关于axios封装细节
import axios from "axios"
import store from "@/store/index"
import { Message } from "element-ui"
const http = {}
const instance = axios.create({
timeout: 5000
})
//创建axios实例对象
//添加请求拦截器
instance.interceptors.request.use(
function (config) {
if (store.state.defaultState.UserToken) {
//给请求头添加权限验证
config.headers.Authorization = store.state.defaultState.UserToken
}
console.log(config);
return config//以便这个修改后的配置能够继续传递给下一个拦截器或最终的请求调用处
},
function (err) {
return Promise.reject(err)
}
)
//响应拦截器
instance.interceptors.response.use(
response => {
console.log("response:", response);
return response.data
},
err => {
if (err && err.response) {
switch (err.response) {
case 400:
err.message = "请求错误"
break;
case 401:
Message.warning({
message: "授权失败,请重新登录"
})//弹出警告信息
store.commit('LOGIN_OUT')
setTimeout(() => {
window.location.reload()
}, 1000)
return
case 403:
err.message = '拒绝访问'
break
case 404:
err.message = '请求错误,未找到该资源'
break
case 500:
err.message = '服务端出错'
break
}
} else {
err.message = '连接服务器失败'
}
Message.error({
message: err.message
})//弹出错误信息
return Promise.reject(err.response)
}
)
http.get = function (url, options) {
return new Promise((resolve, reject) => {
instance
.get(url, options)
.then(response => {
//这里的response是拦截器返回的response.data,包含后端传来的所有json信息
if (response.code === 0) {
resolve(response.data)
//这里resolve会传回给调用的await的变量,这里的data就是json里的data,是权限对应的路由
} else {
Message.error({
message: response.message
})//弹出错误信息
reject(response.message)
}
})
.catch(err => {
console.log(err);
})
})
}
http.post = function (url, options) {
return new Promise((resolve, reject) => {
instance
.post(url, data, options)
.then((response) => {
if (response.code === 0) {
resolve(response.data)
} else {
Message.error({
message: response.message
})//弹出错误信息
reject(response.message)
}
})
.catch(err => {
console.log(err);
})
})
}
export default http
关于api封装
src
|--api
|--index.js
import http from '../utils/http';
import store from '../store/index'
export function fetchPermission() {
return http.get("/api/fetchPermission?user=" + store.state.defaultState.UserToken)
}//因为这个方法是在actions中调用的,所以这里的参数也是在store中获取
export function login(user) {
return http.get("/api/login?user=" + user)
}
关于代理服务器配置-解决跨域
const devServerConfig = {
devServer: {
proxy: {
'/api': {
target: "http://localhost:3300",
//所有以 /api 开头的请求都会被转发到 http://localhost:3300 这个地址上的服务器
changeOrigin: true,
//如果设置为 true,会添加 Origin 字段,并将其设置为目标服务器的地址
pathRewrite: {
'^/api': ''
//它的作用是将匹配到的请求路径中的 /api 替换为空字符串,即去掉了原始请求路径中的 /api 部分
}
}
}
}
};
关于后端node.js配置
const express = require('express')
const app = express()
const admin = require('./data/admin.json')
const member = require('./data/member.json')
const admin_Permission = require('./data/admin_permission.json')
const member_Permission = require('./data/member_permission.json')
// const url = require("url")//要根据用户返回的参数不同返回不同的数据,express框架集成好了,不需要引入了
// app.get('/login',(res,req)=>{
// const user=url.parse(req.url,true).query.user
// })
app.get('/login', (req, res) => {
const user = req.query.user
console.log(user);
if (user == 'admin') {
res.send(admin)
} else {
res.send(member)
}
})
app.get('/fetchPermission', (req, res) => {
const user = req.query.user
console.log(user);
if (user == 'admin') {
res.send(admin_Permission)
} else {
res.send(member_Permission)
}
})
app.listen(3300, () => {
console.log(3300);
})
Step4:Token与权限列表存储
关于Token存储
export default {
get UserToken() {
return localStorage.getItem("token")
},
set UserToken(val) {
localStorage.setItem("token", val)
}
}
//这里使用get和set方法是对象的一种用法,设置对象的属性值,可以直接用
//对象.属性值=xxx调用set方法
//对象.属性值 调用get方法
export default {
LOGIN_IN(state, token) {
state.defaultState.UserToken = token
},
LOGIN_OUT(state) {
state.defaultState.UserToken = ""
}
}
//导出时采用解构赋值的形式或者是直接将token的这些state,mutations作为一个modules直接放到配置中
这里补充一点modules中的state、mutation和dispatch如何调用
// 访问 Vuex store 中的状态
const myState = this.$store.state.myModule.myState; // 以模块化的方式访问状态
const globalState = this.$store.state.globalState; // 全局状态的访问
// 提交 mutations
this.$store.commit('myModule/MY_MUTATION', payload); // 以模块化的方式提交 mutations
this.$store.commit('MY_GLOBAL_MUTATION', payload); // 全局 mutations 的提交
// 分发 actions
this.$store.dispatch('myModule/myAction', payload); // 以模块化的方式分发 actions
this.$store.dispatch('myGlobalAction', payload); // 全局 actions 的分发
mapState(namespace: string, map: Array<string> | Object<string>)
mapMutations(namespace: string, map: Array<string> | Object<string>)
mapActions(namespace: string, map: Array<string> | Object<string>)
//=====================================用例===========================================
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const userModule = {
state: {
currentUser: null,
},
mutations: {
SET_CURRENT_USER(state, user) {
state.currentUser = user;
},
},
actions: {
fetchUser({ commit }, userId) {
// Some async operation to fetch user data
// then commit mutation to set the user data in the state
commit('SET_CURRENT_USER', { id: userId, name: 'John Doe' });
},
},
};
export default new Vuex.Store({
modules: {
user: userModule,
// ... other modules
},
});
//===========================================================================//
<template>
<div>
<p v-if="currentUser">Logged in as {{ currentUser.name }}</p>
<button @click="loginUser">Login</button>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState('user', ['currentUser']), // Maps 'user/currentUser' to local computed property 'currentUser'
},
methods: {
...mapActions('user', ['fetchUser']), // Maps 'user/fetchUser' to local method 'fetchUser'
async loginUser() {
await this.fetchUser(123); // Dispatches the 'fetchUser' action defined in 'user' module
},
},
};
</script>
关于权限列表存储
import { fetchPermission } from "@/api/index.js";
import router, { DynamicRoutes } from '../../router/index';
import { mydynamicRoutes } from "../../router/dynamic-router"
import { recursionRouter, setDefaultRoutes } from "@/utils/recursion-router";
const _ = require('lodash');//node.js环境下的强大的深拷贝工具
export default {
namespaced: true,
state: {
permissionList: null,
sidebarMenu: [],
currentMenu: '',
},
getters: {},
mutations: {
SET_PERMISSION(state, routes) {
state.permissionList = routes;
},
CLEAR_PERMISSION(state) {
state.permissionList = null;
},
SET_MENU(state, menu) {
state.sidebarMenu = menu;
},
CLEAR_MENU(state) {
state.sidebarMenu = []
},
},
//{ commit, state }实际上是解构赋值了context
actions: {
async FETCH_PERMISSION({ commit, state }) {
//发送网络请求获取权限列表(获取->处理->存储)
//1.获取
let permissionList = await fetchPermission()
//2.处理-筛选出符合条件的路由
let routes = recursionRouter(permissionList, mydynamicRoutes)
//2.处理-深拷贝一份原生的动态路由as A,这里为什么怎么做是为了能不直接操作DynamicRoutes
//以便后续退出再登录能够还原初始状态,从而免去了移除路由的一步
//那么就可以对A进行添加子路由/同级路由操作。
//实际上这一步就是造一个你想要的动态路由该有的样子
let DynamicRoutesCopy = _.cloneDeep(DynamicRoutes)//深拷贝引入的动态路由
let children = DynamicRoutesCopy.find(item => item.path === "/").children
children.push(...routes)
//生成菜单,调用SET_MENU这个mutations就可以生成菜单了
commit("SET_MENU", children)
//3.添加你处理好的动态路由
DynamicRoutesCopy.forEach(route => {
router.addRoute(route);
});
//获取用户的默认路由配置
let initialRoutes = router.options.routes
//将所有路由信息存储到permissionList中,表示该用户的所有路由
commit("SET_PERMISSION", [...initialRoutes, ...DynamicRoutesCopy])
}
}
}
关于路由筛选算法
/**
* 方法一:比对路由权限(后台返回的和前台定义的去对比,取出对比一样的结果)
*/
/**
*
* @param {Array} userRouter 用户后台路由
* @param {Array} allRouter 前台所有路由
* @return {Array} realRoutes 符合条件的路由
*/
export function recursionRouter(userRouter = [], allRouter = []) {
let realRoutes = []
allRouter.forEach((all_item, index) => {
userRouter.forEach((user_item, index) => {
if (user_item.name === all_item.meta.name) {
if (user_item.children && user_item.children.length > 0) {
all_item.children = recursionRouter(user_item.children, all_item.children)
}
realRoutes.push(all_item)
}
})
})
return realRoutes
}
中途遇到的bug
1.关于预解析
vue在预编译会把代码先执行,所以你看到的代码不一定就是首次渲染的代码
2.关于深拷贝
这里是我的理解误区:
无法解析 动态引入的代码import(),所以这里没有component选项
可以解析,被提前引入的变量
手动添加对比
DynamicRoutesCopy[0].component = Main
对比下来,问题其实还是在于深拷贝的问题,深拷贝没有办法拷贝被引入的变量,有点复杂,画个图解释,这里拷贝的A'的并不是a.js里的A,因为深拷贝无法完全拷贝
必须要在本来已经定义好的路由直接执行添加或者删除,因为通过深拷贝虽然能够真正地隔离原路由,但是做不到所有都拷贝,所以只有一种解决方法,那就是直接在暴露过来的路由进行操作
修正我的认知错误
修正我之前的说法,完全是因为之前的深拷贝方式有问题:
let DynamicRoutesCopy =JSON.parse(JSON.stringify(DynamicRoutes));
//对比
const _ = require('lodash');
let DynamicRoutesCopy = _.cloneDeep(DynamicRoutes)
- JSON.parse(JSON.stringify(obj))
-
- 这种方法利用了 JSON 序列化和反序列化的过程来完成深拷贝。它适用于绝大部分的简单对象和数组。优点是简单易用,并且对于大多数情况都能正常工作。但是,它有一些限制,比如不能处理特殊的对象属性(如函数、undefined、Symbol 等),并且会忽略对象的原型链上的属性。
- _.cloneDeep(obj) (Lodash)
-
- Lodash 提供的 _.cloneDeep() 方法是一个强大且完善的深拷贝函数,能够处理几乎所有类型的对象,包括对象、数组、函数、日期、正则表达式等。它能够完整地复制对象及其原型链上的属性,并且能够处理循环引用的情况。相比于简单的 JSON 序列化方法,Lodash 的深拷贝函数更全面且健壮。
总的来说,如果你的对象中只包含基本数据类型和普通的对象属性,并且没有函数、日期等特殊类型,使用 JSON 序列化方法是个简单而有效的选择。但如果你的对象包含了各种特殊类型的属性,或者需要更全面、健壮的深拷贝方式,建议使用 Lodash 的 _.cloneDeep() 方法。
可以做到完全拷贝,是因为JSON.parse(JSON.stringify(obj))
这个方法的局限性,如果使用require('lodash')
可以做到真正的深拷贝,就可以采用动态引入