我这里是vue3开发的一个后台管理系统,所以涉及用户权限管理,以及页面权限等,其他模块部分可以查看专栏,这里只对怎么实现根据用户权限控制左侧菜单和路由。
????????用户权限控制路由表和左侧菜单一般是带权限的系统的通用功能,在我还在上大学的时候,实现这个功能感觉很费劲,因为那时候对Vue一知半解,很多权限模模糊糊地实现了,实际上我现在也不能说全部了解,但是经过了几个项目,脑子里有了大概的思路。? ? ?
用户权限我认为大可以分成两类:控制访问哪些页面,控制能执行哪些操作。
????????前者要实现用户只能查看有权限的页面,比如,左侧菜单的菜单栏都是根据权限过滤的,跳转路由时要判断权限(路由守卫的工作),后者要做到用户只能操作有权限的功能,比如点击新建、修改按钮等。后者实现也很简单,好的方法是加一个权限的指令,哪个地方需要判断权限就调用,这里先只讨论前者的实现。
????????如何实现这个功能,拆解为两部分,首先我们要知道用户有哪些菜单的权限,通常是通过接口获取,接着我们要知道用户有哪些路由的权限,比如'/login','/401','/user',这些页面,当然有时候401,500这些页面也会配置在白名单里,不受权限管理。
总结有两部分的权限:菜单权限,路由权限。
这里实现的功能如下:
用户有菜单A,B,C的权限,那么左侧菜单就显示ABC,
然后用户有路由 '/A','/B','/C','/D','/401','/login' 的权限,
那么用户可以访问这几个页面。
假如,用户没有'/B'的权限,那么点击菜单B也会跳到401页面。
? 具体的功能明白了,那就开始详细的开发吧。
我先说下我具体的思路。
????????首先用户需要获取两部分权限,加上这两个接口,在哪里调用呢,肯定是左侧菜单渲染前,路由表生成前。
????????前者应该在sidemenu的组件页进行,一般我们都会有这个页面对吧,根据获取到的左侧菜单,进行渲染,这样就完成了第一部分工作。
????????至于路由权限的控制,那么肯定需要进行路由守卫的帮助,所以一般我会在router下进行,添加路由守卫拦截,每次router.push的时候,判断一下是否有权限。至于权限从哪里来,我这里是从用户的信息里获取到的,一般系统会有获取用户信息的接口吧,这里会有一些用户的个人配置和基本信息。
????????我是在登录后,获取用户信息,存在了store里,然后nvabar里也获取了用户信息,执行同样的操作,然后这样就避免了每次刷新之后信息丢失的问题,当然用户信息可以放在storage或者cookie里,但是为了信息安全,还是别这样做了。
左侧菜单的显隐我是根据权限显示的,没有权限则,没有这个菜单项,这样比较简单。
我显示一下我的数据格式,我获取到的菜单权限是一个数组,每一个对象里有对应路由的id和名字,我需要根据权限显示对应的菜单,如下:
?
这里是全部的路由表的数据:
?
其中我将需要权限的页面都放在了path='/'的children里。?
下面我实现的具体代码吧
const routes = ref()
onMounted(async () => {
await getPermissionRoutes()
routes.value = handleAdminMenus()
})
const handleAdminMenus = () => {
const filteredRoutes = allRoutes.value.reduce((acc, route) => {
if (route.path == '/') {
const filteredChildren = route.children.filter((child) => {
return adminMenus.value.some((item) => item.table_id === child.name)
})
if (filteredChildren.length > 0) {
acc.push({ ...route, children: filteredChildren })
}
}
return acc
}, [])
return filteredRoutes[0].children
}
let adminMenus = ref()
const getPermissionRoutes = async () => {
await getAdminMenus().then((res) => {
adminMenus.value = res
console.log(adminMenus.value, res)
return adminMenus.value
})
}
<template>
<div id="Sidebar" class="reset-menu-style">
<!--logo-->
<Logo v-if="settings.sidebarLogo" :collapse="!sidebar.opened" />
<!--router menu-->
<el-scrollbar>
<el-menu
class="el-menu-vertical"
:collapse="!sidebar.opened"
:default-active="activeMenu"
: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>
我上面主要是将获取到权限菜单和路由表做了一个对应比较,只让菜单显示有权限的路由,那么自然也只能点击有权限的路由了,比如菜单ABC,当然如果输入路径‘/404’,此刻也是可以跳转的,如何后面控制了路由守卫,如果没有404的页面权限,那么输入404 ,应该会跳到401页面,这里先说明一下。
接下来我们就实现这部分功能——只能根据权限进入相应的页面
这里的还是比较简单的,主要就以下一些功能
当然这是比较简单的一个拦截,如果需要做一些其他操作比如,生成动态路由,或者添加权限等,都可以在【判断是否有当前权限】这之后进行。
import router from '@/router'
import { progressClose, progressStart } from '@/hooks/use-permission'
import { useBasicStore } from '@/store/basic'
import { langTitle } from '@/hooks/use-common'
import settings from '@/settings'
import { Message } from '@/hooks/use-element'
import { userInfoReq } from '@/api/user'
//路由进入前拦截
//to:将要进入的页面 vue-router4.0 不推荐使用next()
const whiteList = ['/login', '/404', '/401', '/loading'] // no redirect whitelist
router.beforeEach(async (to) => {
progressStart()
document.title = langTitle(to.meta?.title) // i18 page title
const basicStore = useBasicStore()
// not login
if (!settings.isNeedLogin) {
// basicStore.setFilterAsyncRoutes()
return true
}
if (whiteList.indexOf(to.path) !== -1) {
return true
}
// 是否已登录
if (basicStore.token) {
// 已登录
if (to.path === '/login') {
return '/'
} else {
const isGetUserInfo = basicStore.getUserInfo
// 是否已获取用户信息和权限
if (isGetUserInfo) {
// 判断权限
const userPermissions = basicStore.userPermissions || []
const hasPermission = userPermissions.some((permission) => permission === to.path)
if (hasPermission) {
// basicStore.setFilterAsyncRoutes()
return true
} else {
// 没有权限,跳转到 401 页面
return '/401'
}
} else {
try {
// 获取用户信息和权限
userInfoReq()
.then((res) => {
basicStore.setUserInfo({
userInfo: res
})
// 判断权限
const userPermissions = basicStore.userPermissions || []
const hasPermission = userPermissions.some((permission) => permission === to.path)
if (hasPermission) {
// basicStore.setFilterAsyncRoutes()
return true
} else {
// 没有权限,跳转到 401 页面
return '/401'
}
})
.catch((err) => {
basicStore.setUserInfo({
userInfo: {}
})
Message(err || '获取用户信息失败', 'error')
return '/401'
})
} catch (err) {
console.log('permission-catch-error', err)
return `/login?redirect=${to.path}`
}
}
}
} else {
// 未登录,跳转到登录页面
if (!whiteList.includes(to.path)) {
return `/login?redirect=${to.path}`
} else {
return true
}
}
})
//路由进入后拦截
router.afterEach(() => {
progressClose()
})
这里留了一个口子,basicStore.setFilterAsyncRoutes(),如果登录后需要生成动态路由,则可以在这里进行。,但是我们的路由比较简单,就不做这些啦。
这里补充一些登录后的路由跳转到权限控制
系统里我写了一个loading页面,系统登录后先跳到这个页面,这个页面会获取当前用户的菜单权限和用户权限,这个页面的作用如下:
如果当前菜单首页,那么这时会跳至首页。
如果当前有权限的菜单有BCD里没有默认跳转页面,那么就根据当前的菜单顺序,跳转至第一个菜单页,那么会去往B页面。
所以这个页面的目的也很明显啦,就是一个中转页,如果系统不小心去往了401,404,那么返回首页的时候,这个首页也会去往loading,这样就可以在用户没有首页权限时,也能有一个当前权限的【首页】,总要给用户一个页面的去处嘛。?
这些功能的实现也很简单,如下:
let adminMenus = ref()
onMounted(async () => {
if (basicStore.adminMenus?.length > 0) {
adminMenus.value = basicStore.adminMenus
goToPath()
} else {
await getPermissionRoutes()
goToPath()
}
})
const goToPath = () => {
const userPermissions = basicStore.userPermissions || []
const hasPermission = userPermissions.some((permission) => permission === adminMenus.value[0].table_id)
if (userPermissions.some((permission) => permission === '/homePage')) {
router.push(`/homePage`)
} else {
if (adminMenus.value[0].type === 'page' && hasPermission) {
router.push(adminMenus.value[0].table_id)
} else if (hasPermission) {
router.push(`/t${adminMenus.value[0].table_id}`)
}
}
}
其实权限控制一般都跟业务关联很强,经常会在基础的上面那种权限控制上添加新的,所以很多时候在后期加新需求时,哪怕对权限很熟悉,也会有时候遇到一些bug,只要熟悉每一个组件的加载顺序,多加打印,就能慢慢解决~