文章前言
在基础的 vue 项目中都是默认以前端配置好静态路由
但在一些较为成熟或大型项目中我们会通常会遇到 权限控制
这个问题,这样我们就涉及到动态路由
的设置了
特别是一些 后台管理系统项目,需要对不同的用户进行角色区分,对不同的角色的操作权限进行限制
如果单纯按照前端配置好的静态路由,就会出现用户登陆后,即使没看到页面的入口,也可以通过输入游览器地址url进入被禁止的页面,因为在项目运行时,路由边已经生成了,所以我们需要对需要权限控制的路由进行动态的配置生成
有2两种的方法对路由进行动态设置
1、简单角色路由:根据服务器返回的用户角色权限,前端本地加载不同的路由配置生成路由
2、复杂权限路由:角色页面权限由服务器控制,服务器返回路由页面数据信息,前端根据路由信息生成路由
简单角色动态路由
首先后台系统一般都有一个基础 layout 布局入口页面,再从该页面中去衍生子菜单路由,展示页面入口,本篇将首页配置为布局layout页面,再从主页面中展示具体的子路由页面
1、配置路由文件,定义路由信息
将不需要权限的路由信息写入 routes 数组并生成,例如登录login
将动态配置的权限路由写到 asyncRoutes 数组
/src/router/index.js
js
import Vue from "vue";
import VueRouter from "vue-router";
import store from "@/store";
Vue.use(VueRouter);
// 默认静态路由
const routes = [
{
path: "/login",
name: "login",
component: () => import("@/views/login/index.vue"),
},
];
// 动态配置的路由(配置权限)
export const asyncRoutes = [
{
path: "/test1",
name: "Test1",
component: () => import("@/views/test1/index.vue"),
meta: {
roles: ["admin", "editor"], // 拥有这些角色权限的用户才能访问
title: "测试1",
},
children: [
{
path: "children1",
name: "Test1Children1",
component: () => import("@/views/test1/test1-children1/index.vue"),
meta: { roles: ["admin"], title: "测试1子路由1" },
},
{
path: "children2",
name: "Test1Children2",
component: () => import("@/views/test1/test1-children2/index.vue"),
meta: { roles: ["editor"], title: "测试1子路由2" },
},
{
path: "children3",
name: "Test1Children3",
component: () => import("@/views/test1/test1-children3/index.vue"),
meta: { roles: ["admin"], title: "测试1子路由3" },
},
],
},
{
path: "/test2",
name: "Test2",
component: () => import("@/views/test2/index.vue"),
meta: { roles: ["admin"], title: "测试2" },
children: [],
},
];
// 创建一个路由器实例
const createRouter = () =>
new VueRouter({
mode: 'hash',
routes,
});
// 创建路由器
const router = createRouter();
// 重置路由器
export function resetRouter() {
const newRouter = createRouter()
// 重置router对象
router.matcher = newRouter.matcher;
// 提交用户路由的清除状态的mutation
store.commit("user/CLEAR_ROUTERS");
}
export default router;
2、permission模块,集成封装根据权限过滤路由的vuex方法
创建vuex模块 permission.js
写入generateRoutes
方法,方便项目中全局调用,传入角色权限数组
来进行路由过滤
根据 权限角色 来 过滤动态配置路由 得到需要生成的路由信息
/src/store/modules/permission.js
js
import { asyncRoutes } from "@/router";
const state = {
routes: [], // 动态配置的路由
};
const mutations = {
SET_ROUTES: (state, routes) => {
state.routes = routes;
},
};
const actions = {
/**
* 生成路由
* @param {Object} commit - 提交方法
* @param {Array} roles - 用户角色权限数组
* @returns {Promise} - 返回一个Promise对象,用于异步处理
*/
generateRoutes({ commit }, roles) {
return new Promise((resolve) => {
let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles); // 传入动态配置路由数组,角色权限数组
console.log(accessedRoutes, 'accessedRoutes')
commit("SET_ROUTES", accessedRoutes);
resolve(accessedRoutes);
});
},
};
export default {
namespaced: true,
state,
mutations,
actions,
};
在action异步 generateRoutes 方法中调用 filterAsyncRoutes 方法对 asyncRoutes 数组进行过滤
filterAsyncRoutes 方法使用递归进行过滤直到没有children字段为止
js
/**
* 使用 meta.role 确定当前用户是否具有权限
* @param {Array} roles - 用户的角色列表
* @param {Object} route - 路由对象
* @returns {boolean} - 如果用户具有访问权限则返回true,否则返回false
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
/**
* 通过递归筛选异步路由表
* @param routes - asyncRoutes 动态配置的路由
* @param roles - 用户的角色列表
*/
export function filterAsyncRoutes(routes, roles) {
// 初始化结果数组
const res = []
// 遍历路由数组
routes.forEach(route => {
// 创建临时变量,复制route对象
const tmp = { ...route }
// 判断是否有权限
if (hasPermission(roles, tmp)) {
// 如果有权限,继续遍历子路由
if (tmp.children) {
// 递归调用filterAsyncRoutes函数,过滤子路由
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
// 将符合条件的路由添加到结果数组中
res.push(tmp)
}
})
// 返回结果数组
return res
}
3、user模块,模拟获取用户信息方法
创建vuex模块 user.js ,用于模拟接口获取用户角色信息并保存状态
/src/store/modules/user.js
js
const state = {
userInfo: {}, // 用户信息
roles: [], // 角色权限数组
};
const mutations = {
SET_USERINFO(state, userInfo) {
state.userInfo = userInfo;
},
SET_ROLES(state, roles) {
state.roles = roles;
},
};
const actions = {
// 获取用户信息方法
getInfo({ commit }) {
return new Promise((resolve) => {
const data = {
name: "admin",
avatar: "",
roles: ["admin"],
introduction: "I am a super administrator",
status: 1,
email: "",
phone: "1234567890",
id: 1,
createTime: "2016-11-22 10:30:30",
lastLoginTime: "2019-05-27 10:30:30",
lastLoginIp: "127.0.0.1",
};
commit("SET_USERINFO", data); // 设置用户信息
commit("SET_ROLES", data.roles); // 设置角色权限
// 延时模拟异步
setTimeout(() => {
resolve(data);
}, 500);
});
},
};
export default {
namespaced: true,
state,
mutations,
actions,
};
返回['admin'] 角色权限时 ,过滤后的路由输出如下,返回包含admin权限的路由

4、创建全局路由守卫,控制路由权限并动态生成路由
定义好了上述的一系列方法,那么在什么时机调用,并且添加到路由中呢?
1.正确的时机应当是 用户已经登录并取得得用户信息后,再根据用户角色过滤路由,最后生成路由
2.当用户退出登录或者未登录时,清空用户信息以及路由,恢复为初始状态,跳转回登录页
使用 vue-router的全局路由守卫 定义一个路由权限文件 进行权限控制
如下代码为假设已经登录获取token或sessionId用户认证的情况下
/src/router/permission.js
js
import router, { resetRouter } from "@/router";
import store from "@/store";
const whiteList = ["/login"]; // 路由白名单,无需登录便可以访问的路由
router.beforeResolve(async (to, from, next) => {
// 获取用户登录的token
const hasToken = "xxxxxxxx"; // getToken() // 这里用虚拟值代替,表示已登录
// 判断当前用户是否登录
if (hasToken) {
if (to.path === "/login") {
// 如果当前用户已经登录,则跳转到首页
next({ path: "/" });
} else {
// 从store user模块中获取用户权限角色,如果有角色则代表已经获取了用户信息登录中,直接放行
const hasRoles = store.state.user.roles && store.state.user.roles.length > 0;
if (hasRoles) {
next();
} else {
// 获取了token,但是没有角色,则调用获取用户信息接口获取角色权限数组
try {
// 调用获取存储用户信息,并取得角色权限数组,例如 ['admin'] or ,['developer','editor']
const { roles } = await store.dispatch("user/getInfo");
// 获取用户角色权限数组后调用vuex的generateRoutes方法过滤掉没有权限的路由表并返回
const accessRoutes = await store.dispatch("permission/generateRoutes", roles);
// 使用addRoute将 动态配置的路由 添加到 layout 首页子级中
router.addRoute({
path: "/",
name: "home",
redirect: accessRoutes[0].path,
component: () => import("@/views/layout"),
meta: { title: "首页" }, // 路由元信息
children: accessRoutes,
});
// 然后再添加404导航路由,防止刷新后默认导向404页
router.addRoute({
path: "/404",
redirect: "/404",
hidden: true,
component: () => import("@/views/notFoundPage.vue"),
});
router.addRoute({ path: "*", redirect: "/404", hidden: true });
// 设置replace:true,这样导航就不会留下历史记录
next({ ...to, replace: true });
} catch (error) {
// 捕获错误异常退出登录清除用户信息
resetRouter(); // 重置路由
// await store.dispatch("user/resetToken");
next(`/login?redirect=${to.path}`);
}
}
}
} else {
// 用户未登录
resetRouter(); // 重置路由
if (whiteList.indexOf(to.path) !== -1) {
// 需要跳转的路由是否是白名单whiteList中的路由,若是,则直接跳转
next();
} else {
// 需要跳转的路由不是白名单whiteList中的路由,直接跳转到登录页
next(`/login?redirect=${to.path}`);
}
}
});
最终效果如下:

核心小结
核心流程为以下的步骤
1、在路由全局前置守卫中,首先获取token
或者sessionId
等身份认证信息,表示已登录,未登录并且不在白名单内的路由统一跳转回登录页
2、在已登录环境作用域
中 判断是否已经取得用户信息,例如本篇需要用到的用户权限角色,如果存在角色权限信息则代表已经处于登录后生成了动态路由配置。如果没有,则进行下一步
3、从vuex 获取用户权限角色后,再调用 过滤路由的方法,返回过滤后的路由信息调用addRoute()
添加路由,并且为防止页面刷新时会重置路由后找不到对应的路由地址会导向404页,所以将404页放在 等待接口返回用户信息获取完成,动态添加路由后 再添加
复杂权限路由
当系统角色比较多,页面路由较多较为复杂时,如果继续让前端在本地配置路由的方式就会显得比较臃肿麻烦
所以衍生出另外一种方式,由后端数据库返回路由信息,前端根据数据动态添加路由,统一动态配置
这样每当角色更改,角色可访问路由发生变化时,只需对后台数据进行修改,无需前端进行系统更新,省去了一波麻烦事
具体实现逻辑和上述讲的大同小异,区别在于获取路由信息的不同
1、模拟返回mock路由信息数据
首先定义一组模拟接口返回的mock路由数据,用在获取用户信息接口中
/src/utils/routes.js
js
export default [
{
path: "/test1",
name: "Test1",
meta: { title: "测试1" },
children: [
{
path: "test1-children1",
name: "Test1Children1",
meta: { title: "测试1子路由2" },
},
{
path: "test1-children2",
name: "Test1Children2",
meta: { title: "测试1子路由2" },
},
],
},
{
path: "/test2",
name: "Test2",
children: [],
meta: { title: "测试2" },
}
];
在原有文件,上面例子vuex的user模块文件中,在getInfo()获取用户信息方法中模拟返回
/src/store/modules/user.js
js
import routes from '@/utils/routes.js'
const actions = {
// 获取用户信息
getInfo({ commit }) {
return new Promise((resolve) => {
const data = {
name: "admin",
avatar: "",
roles: ["admin"],
introduction: "I am a super administrator",
status: 1,
email: "",
phone: "1234567890",
id: 1,
createTime: "2016-11-22 10:30:30",
lastLoginTime: "2019-05-27 10:30:30",
lastLoginIp: "127.0.0.1",
routes, // 路由信息数组
};
commit("SET_USERINFO", data); // 设置用户信息
commit("SET_ROLES", data.roles); // 设置角色权限
// 延时模拟异步
setTimeout(() => {
resolve(data);
}, 500);
});
},
};
由后端接口返回路由信息数组后,前端就不用自己根据角色进行过滤,而是根据数组数据匹配本地目录生成路由
2、全局路由守卫匹配文件目录并添加路由
在原有文件,上面例子全局路由守卫中进行修改
/src/router/permission.js
js
// 调用获取存储用户信息,并取得可访问的路由数组
const { routes } = await store.dispatch("user/getInfo");
const routers = generateRouters(routes); // 匹配本地文件目录路由
store.commit("permission/SET_ROUTES", routers); // 将路由表存入vuex
// 使用addRoute将 动态配置的路由 添加到 layout 首页子级中
router.addRoute({
path: "/",
name: "home",
redirect: routers[0].path,
component: () => import("@/views/layout"),
meta: { title: "首页" }, // 路由元信息
children: routers, // 匹配后的路由数组
});
定义 generateRouters方法,使用递归,对接口返回的路由数组进行文件目录匹配
js
/**
* 匹配本地文件目录路由
* @param {Array} routes - 路由数组
* @param {string} parentPath - 父路径
* @param {number} level - 层级,默认为0
* @returns {Array} - 生成的路由数组
*/
function generateRouters(routes, parentPath = '', level = 0) {
routes.forEach((item) => {
let path // 匹配文件路径
if (level === 0) { // 第一层路由
path = item.path
item.component = () => import(`@/views${path}/index.vue`);
} else { // 子路由
path = parentPath + `/${item.path}`
item.component = () => import(`@/views${path}/index.vue`);
}
if (item.children && item.children.length > 0) {
item.children = generateRouters(item.children, path, level + 1);
}
})
return routes
}
layout 主页面处理
最后,处理layout主页面如何显示
本篇先放出实现的代码,做一下简洁预览,实际业务要根据后端的数据结构进行更改
使用element ui,递归子菜单item组件来进行展示
首页代码
/src/views/layout/index.vue
html
<template>
<el-row class="tac">
<el-col :span="4">
<el-menu
:default-active="defaultActive"
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<SidebarItem v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-col>
<el-col :span="20">
<router-view></router-view>
</el-col>
</el-row>
</template>
<script>
import SidebarItem from './components/SidebarItem.vue'
export default {
components: {
SidebarItem,
},
data() {
return {
defaultActive: "/dashboard",
};
},
watch: {
// 监听路由变化
$route(route) {
this.defaultActive = route.name;
}
},
computed: {
permission_routes() {
return this.$store.state.permission.routes // 动态配置的路由
},
},
created() {
this.defaultActive = this.$route.name; // 初始化已选中菜单
},
};
</script>
菜单子项item组件代码
菜单子项item处理的组件
/src/view/layout/components/SidebarItem.vue
html
<template>
<div v-if="!item.hidden">
<template v-if="!item.children || item.children.length === 0" >
<el-menu-item :index="item.name" @click="toggleMenu(item)">
<span slot="title">{{ item.meta.title }}</span>
</el-menu-item>
</template>
<el-submenu v-else :index="item.path">
<template slot="title">{{ item.meta.title }}</template>
<SidebarItem
v-for="child in item.children"
:key="child.path"
:item="child"
:base-path="child.path"
/>
</el-submenu>
</div>
</template>
<script>
export default {
name: "SidebarItem",
props: {
// 当前 route 对象
item: {
type: Object,
required: true,
},
// 当前 route 路径
basePath: {
type: String,
default: "",
},
},
data() {
return {
routeName: ""
}
},
watch: {
// 监听路由变化
$route(route) {
this.routeName = route.name;
}
},
created() {
this.routeName = this.$route.name;
},
methods: {
toggleMenu(item) {
// 当前路由与菜单项路由的name相等时,不做任何处理
if (this.routeName === item.name) {
return
}
// 否则,执行路由跳转
this.$router.push({
name: item.name,
});
},
},
};
</script>
