背景:当项目面向的对象不只是一个角色的时候,需要我们考虑根据角色的不同,页面功能导航栏选项也要不同。这样可以避免跨权限问题。保证了在一个项目里面供多个角色都能安全使用。
举例子:公司培训管理系统,涉及到:学生、讲师、团队领导三种角色,而且一个人可以是双重角色。比如A学生可能是B学生的讲师,则A既是学生又是老师。学生具有选课功能,而讲师没有。
我们看如何实现:
动态添加路由
1.先要做的是和后端商量好用什么数字代替角色。比如学生1,讲师2,团队管理3
2.划分公共路由区域和动态路由区域。公共路由区域就是值无论是什么角色,即便你没登录的情况也会显示的页面,比如登录注册,404页面。
如下:
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
// 公共的路由区域
export const constantRoutes = [
{
path: "/login",
component: () => import("@/views/login/index"),
hidden: true,
},
{
path: "/404",
component: () => import("@/views/404"),
hidden: true,
},
// 首页路由
{
path: "/",
component: Layout,
redirect: "/announcement",
children: [
{
path: "home",
name: "Home",
component: () => import("@/pages/home/home"),
meta: {
title: "首页",
icon: "主页",
},
// hidden: true
},
{
path: "announcement",
name: "Announcement",
component: () => import("@/pages/announcement/index"),
meta: {
title: "公告",
},
hidden: true, // 不在侧边栏中显示
},
],
},
];
/**
* 动态路由
*/
export const asyncRoutes = [
// 个人中心路由
{
path: "/person",
component: Layout,
redirect: "/person",
children: [
{
path: "person",
name: "Person",
component: () => import("@/pages/person/index"),
meta: {
title: "个人中心",
icon: "个人中心",
roles: [1,2,3],//学员讲师团队领导都能查看
},
},
],
},
// 我的团队 ----课表审核
{
path: "/auditing",
component: Layout,
redirect: "/auditing/auditing",
name: "team",
children: [
{
path: "auditing",
name: "Auditing",
component: () => import("@/pages/team/auditing/index"),
meta: {
title: "选课审核",
icon: "选课审核",
roles: [3], //只能团队领导可以
},
},
{
path: "/details/:workerNumber/:index",
name: "details",
meta: {
roles: [3],
},
hidden: true,
props: true,
component: () => import("@/pages/team/auditing-details/index"),
},
],
},
// 进行课程征集
{
path: "/collage",
component: Layout,
redirect: "/collage",
children: [
{
path: "collage",
name: "Collage",
component: () => import("@/pages/teacher/collage/index"),
meta: {
title: "课程征集",
icon: "空白",
roles: [2],//只能讲师
},
},
],
},
// 所有角色都能选课
{
path: "/choice",
component: Layout,
redirect: "/choice",
children: [
{
path: "choice",
name: "Choice",
component: () => import("@/pages/student/choice/index"),
meta: {
title: "培训选课",
icon: "空白",
roles: [1,2,3],//所有角色都能选课
},
},
],
},
// 所有角色都能成绩查询
{
path: "/achievement",
component: Layout,
redirect: "/achievement",
children: [
{
path: "achievement",
name: "Achievement",
component: () => import("@/pages/student/achievement/index"),
meta: {
title: "成绩查询",
icon: "空白",
roles: [ 1,2,3],//所有角色都能成绩查询
},
},
],
},
];
const createRouter = () =>
new Router({
mode: 'history'
routes: constantRoutes,
});
const router = createRouter();
export function resetRouter() {
const newRouter = createRouter();
router.matcher = newRouter.matcher; // 重置路由,一般在用户退出登录的时候会调用
}
export default router;
3.上面做完了,我们只需要获取用户角色信息,再根据role遍历动态路由区域找到符合权限的路由即可。
我们在路由前置守卫做权限判断,只有符合权限的才能放行:
import router, { constantRoutes } from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({
showSpinner: false
}) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
//进度条开启
NProgress.start()
document.title = getPageTitle(to.meta.title)
const hasToken = store.getters.token
if (hasToken) {
//用户已登录
if (to.path === '/login') {
//强制跳转到首页
next({
path: '/'
})
NProgress.done()
} else {
//用户前往非登录页面,其中包括带权限的页面,所以我们必须确保在有权限的前提下放行路由
//判断用户是否获取到自己的权限信息,也就是角色信息
const hasRoles = store.getters.roles && store.getters.roles.length > 0
//判断是否给用户添加了属于他自己的权限路由
const hasAddRoutes =
store.getters.permission.routes &&
store.getters.permission.routes.length === router.options.routes.length
if (hasRoles && hasAddRoutes) {
//用户权限信息有了并且也添加好了属于他的权限路由,那么用户通过点击图形界面的导航模块肯定是用户权限范围内,所以直接放行即可。如果用户在url内输入非权限范围内的地址,那么路由只能匹配到404页面
next()
} else {
if (!hasRoles) {
// 用户登录了但是没有获取到权限有关的信息
try {
// 异步请求获取权限有关的信息
await store.dispatch('user/getInfo')
// 有了用户权限信息之后就获取对应权限路由
const accessRoutes = await store.dispatch(
'permission/generateRoutes',
store.getters.roles
)
//将权限路由模块和静态路由结合起来
router.options.routes = constantRoutes.concat(accessRoutes)
router.addRoutes(accessRoutes)
// 使用next({ …to, replace: true })来确保addRoutes()时动态添加的路由已经被完全加载上去
next({
...to,
replace: true
})
} catch (error) {
// 被捕获的错误有可能是用户的token过期
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
// hasAddRoutes为false
//当用户手动刷新的时候,虽然我们的store设置了本地保存,但是我们之前的router.addRoutes(accessRoutes)添加的accessRoutes没有了,因为只要页面刷新router会重新运行一遍,那么store.getters.permission.routes.length != router.options.routes.length了,需要我们重新获取---不然就会出现404
// 解决页面刷新404问题
console.log('发生了页面刷新')
const accessRoutes1 = await store.dispatch(
'permission/generateRoutes',
store.getters.roles
)
router.options.routes = constantRoutes.concat(accessRoutes1)
router.addRoutes(accessRoutes1)
// 使用next({ …to, replace: true })来确保addRoutes()时动态添加的路由已经被完全加载上去
next({
...to,
replace: true
})
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
NProgress.done()
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
上面没问题就开始上关键的代码:遍历权限路由区域
permission.js
import { constantRoutes, asyncRoutes } from '@/router/index'
// 用于判断某个权限路由模块是否属于当前用户权限内
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some((role) => route.meta.roles.includes(role))
} else {
return true
}
}
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach((route) => {
//浅拷贝一下
const tmp = {
...route
}
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],//保存的是我们将来要要在导航栏渲染的数据!!
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise((resolve) => {
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
accessedRoutes.push(
// 404
{
path: '*',
redirect: '/404',
hidden: true
}
)
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
以上就是关于根据用户角色映射所需的动态路由
之后导航栏的渲染数据就根据store的 permission.js的 state.routes。
<template>
<div>
<el-scrollbar
:noresize="true"
wrap-class="scrollbar-wrapper"
ref="scrollbarWrapper"
id="scrollbarWrapper"
>
<!-- :collapse="isCollapse" -->
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
active-text-color="#ffd04b"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import SidebarItem from "./SidebarItem";
import "@/styles/variables.scss";
export default {
components: { SidebarItem },
computed: {
routes() {
const routes = this.$store.getters.permission_routes;
return routes;
},
activeMenu() {
console.log("路由发生变化啦")
const route = this.$route;
const { meta, path } = route;
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu;
}
return path
}
},
};
</script>
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
created(){
console.log(111,this.item)
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
以上代码经供参考