前言
在写后台管理或者类似的项目中,开发者希望用户只能访问部分路由或者路由中的部分按钮 那么又该如何实现呢
本篇是基于对硅谷甄选项目(可搜到)的代码进行一个总结
菜单权限
方案一:后端动态返回的路由配置
路由的拆分
- 静态路由(常量路由):大家都可以拥有的路由
- 异步路由 :不同身份有的有这个路由 有的没有
- 任意路由 : 比如说用户输入其他路由都跳到404
后端返回路由配置:后端根据用户的不同角色返回不同的路由表(这个在一开始的时候前后端就应该商量好路由的命名,后端的命名要和前端路由的name保持一致)
前端接收路由配置 :通过API调用后端的接口,获取路由配置
前段生成路由 :前段通过vue router 动态添加路由,将后端返回的路由表注入到vue-router中
渲染页面 :前端根据动态添加的路由,展示用户可以访问的页面
后端返回的数据:
下面对路由进行拆分 分为常量 异步 任意
//对外暴露配置路由(常量路由)
export const constantRoute = [
{
//登录路由
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login', //命名路由
meta: {
title: '登录', //菜单标题
hidden: true, //路由的标题在菜单中是否隐藏
},
},
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
hidden: false,
},
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
hidden: false,
icon: 'HomeFilled',
},
},
],
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: {
title: '404',
hidden: true,
},
},
{
path: '/screen',
component: () => import('@/views/screen/index.vue'),
name: 'Screen',
meta: {
hidden: false,
title: '数据大屏',
icon: 'Platform',
},
},
]
//异步路由
export const asnycRoute = [
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
hidden: false,
title: '权限管理',
icon: 'Lock',
},
redirect: '/acl/user',
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
hidden: false,
title: '用户管理',
icon: 'User',
},
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
hidden: false,
title: '角色管理',
icon: 'UserFilled',
},
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
hidden: false,
title: '菜单管理',
icon: 'Monitor',
},
},
],
},
{
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
hidden: false,
title: '商品管理',
icon: 'Goods',
},
redirect: '/product/trademark',
children: [
{
path: '/product/trademark',
component: () => import('@/views/product/trademark/index.vue'),
name: 'Trademark',
meta: {
hidden: false,
title: '品牌管理',
icon: 'ShoppingCartFull',
},
},
{
path: '/product/attr',
component: () => import('@/views/product/attr/index.vue'),
name: 'Attr',
meta: {
hidden: false,
title: '属性管理',
icon: 'CollectionTag',
},
},
{
path: '/product/spu',
component: () => import('@/views/product/spu/index.vue'),
name: 'Spu',
meta: {
hidden: false,
title: 'SPU管理',
icon: 'Calendar',
},
},
{
path: '/product/sku',
component: () => import('@/views/product/sku/index.vue'),
name: 'Sku',
meta: {
hidden: false,
title: 'SKU管理',
icon: 'Orange',
},
},
],
},
]
//任意路由
//任意路由
export const anyRoute = {
//任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由',
hidden: true,
icon: 'DataLine',
},
}
获取路由的方法
注意:拼接数组时,任意路由要放在最后
//硅谷333: routes['Product','Trademark','Sku']
let guigu333 = ['Product', 'Trademark', 'Sku'];
function filterAsyncRoute(asnycRoute, routes) {
return asnycRoute.filter(item => {
if (routes.includes(item.name)) {
if (item.children && item.children.length > 0) {
item.children = filterAsyncRoute(item.children, routes)
}
return true
}
})
}
//硅谷333需要展示的异步路由
let guigu333Result = filterAsyncRoute(asnycRoute, guigu333);
//将常量 异步 任意合并
console.log([...constRoute, ...guigu333Result, anyRoute], '硅谷333');
Product为一级 TradeMark Sku Spu为Product的二级
如果不使用递归 只能筛选出一级 当筛选出Product时,由于三者均为二级路由 并不会再进行深层过滤,最后渲染到页面上三者都在 如果要只展示某几个 要用到递归进行深层筛选并重新赋值给item.children
获取路由完整方法
。。。。。。
import router from '@/router'
//引入路由(常量路由)
import { constantRoute, asnycRoute, anyRoute } from '@/router/routes'
//用于过滤当前用户需要展示的异步路由
function filterAsyncRoute(asnycRoute: any, routes: any) {
return asnycRoute.filter((item: any) => {
if (routes.includes(item.name)) {
if (item.children && item.children.length > 0) {
//硅谷333账号:product\trademark\attr\sku
item.children = filterAsyncRoute(item.children, routes)
}
return true
}
})
}
//创建用户小仓库
const useUserStore = defineStore('User', {
//小仓库存储数据地方
state: (): UserState => {
return {
。。。。。。。
menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)
us。。。。。。
}
},
//处理异步|逻辑地方
actions: {
。。。。。。。
//获取用户信息方法
async userInfo() {
//获取用户信息进行存储
const result: userInfoResponseData = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
//计算当前用户需要展示的异步路由
const userAsyncRoute = filterAsyncRoute(asnycRoute, result.data.routes)
//菜单需要的数据整理完毕
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute];
//目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
[...userAsyncRoute, anyRoute].forEach((route: any) => {
router.addRoute(route)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
。。。。。。
})
//对外暴露小仓库
export default useUserStore
过滤出来每个用户所需要的异步路由(userAsnycRoute) 然后赋值给仓库的menuRoutes
由于这只是将menuRoutes更新 也就是说侧边栏的数据有了 但实际上项目的路由还是只有常量路由,需要动态添加
残留问题
1.深拷贝
之前获取需要的路由方法是浅拷贝,会改变原有的路由
比如说:我现在登录了一个超级权限管理员的账号拥有所有路由 然后又登录了一个只有部分权限(guigu333)的账号 此时经过过滤,menuRoutes只有部分路由展示 然后再次登录超级权限管理员的,就会发现超级权限管理员展示的路由不是全部的路由,而是上次guigu33筛选出的路由
这是因为 :
1.两次登录没有刷新页面 数据也不会进行重新刷新
2.数组是进行的浅拷贝,会改变原有的路由,也就是说asnycRoute被修改了
这里引用深拷贝的方法lodash 需要npm install 进行下载
//引入深拷贝方法
//@ts-expect-error
import cloneDeep from 'lodash/cloneDeep'
。。。。。。
//获取用户信息方法
async userInfo() {
//获取用户信息进行存储
const result: userInfoResponseData = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
//计算当前用户需要展示的异步路由
const userAsyncRoute = filterAsyncRoute(
cloneDeep(asnycRoute),
result.data.routes,
)
//菜单需要的数据整理完毕
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]
//目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
;[...userAsyncRoute, anyRoute].forEach((route: any) => {
router.addRoute(route)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
2.路由加载问题
这样配置路由后,如果你访问的是异步路由,会在刷新的时候出现空白页面。原因是异步路由是异步获取的,加载的时候还没有。因此我们可以在路由守卫文件中改写。这个的意思就是一直加载。
在这里也分享一下路由守卫的代码(其中有进度条)
// 路由鉴权 项目当中路由能不能被访问的权限校验 (某一个路由什么条件下可以访问)
import router from '@/router'
import setting from './setting'
import nprogress from 'nprogress'
// 引入进度条样式
import 'nprogress/nprogress.css'
nprogress.configure({ showSpinner: false })
// 获取用户相关的小仓库内部token数据 去判断用户是否登录
import useUserStore from '@/store/modules/user'
import pinia from './store'
let userStore = useUserStore(pinia)
// 全局守卫:项目当中任意路由切换都会触发的钩子
// 全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
document.title = `${setting.title}-${to.meta.title as string}`
// to:将要访问那个路由
// from:从哪个路由来
// next 路由的放行函数
// 访问某一个路由之前的守卫
nprogress.start()
let token = userStore.token
// 获取用户名字
let username = userStore.username
// 用户登录判断
if (token) {
// 登录成功 访问login 不能访问 指向首页
if (to.path == '/login') {
next({ path: '/' })
} else {
// 登录成功访问其余6个
//有用户信息
if (username) {
next()
} else {
// 没有用户信息 就在守卫这里发请求获取到了用户信息再放行
try {
// 获取用户信息以后再进行放行
await userStore.userInfo()
next({...to})
} catch (error) {
// token过期
// 用户手动修改了本地token
// 那么采用退出登录->清空数据
// 万一:刷新的时候是异步路由,有可能获取到用户信息,异步路由还没有加载完毕,出现空白的效果
await userStore.userLogout()
next({ path: './login', query: { redirect: to.path } })
}
}
}
} else {
// 用户未登录判断
if (to.path == '/login') {
next()
} else {
next({ path: './login', query: { redirect: to.path } })
}
}
next()
})
// 全局后置守卫
router.afterEach((to: any, from: any) => {
nprogress.done()
})
// 问题一:任意路由切换实现进度条业务 ---nprogress
// 问题二: 路由鉴权(路由组件访问权限的设置)
// 全部的路由组件 登录 404 任意 首页 数据大屏 权限管理(三个子路由) 商品管理 (4个子路由)
// 用户未登录 可以访问login 其余6个路由不可以访问
// 用户登录 不可以访问login(指向首页) 其余的路由可以访问
方案二:前端基于角色的路由表
前端配置好完整的路由表,并通过meta对象中设置的roles属性来指定那些角色可以访问某些路由
通过vue-router的导航守卫beforeEach()来权限校验,拦截不符合访问权限的访问请求
jsx
复制代码
const routerMap = [
{
path: '/permission',
component: Layout,
meta: {
title: 'permission',
roles: ['admin', 'editor'] // 允许的角色
},
children: [
{
path: 'page',
component: () => import('@/views/permission/page'),
meta: {
title: 'pagePermission',
roles: ['admin'] // 只有管理员可访问
}
}
]
}
];
router.beforeEach((to, from, next) => {
const userRole = localStorage.getItem('userRole')
if (to.meta.roles && !to.meta.roles.includes(userRole)) {
// 如果用户角色不在路由的 roles 中,跳转到无权限页面
next('/unauthorized')
} else {
// 否则正常跳转
next()
}
})
按钮权限
在某些场景下,除了页面级别的权限控制,按钮级别的控制也是常见的需求,通过自定义指令,可以根据用户的权限动态控制按钮的显示与隐藏
步骤:
1.创建一个src/directive/has.ts文件
import pinia from '@/store'
import useUserStore from '@/store/modules/user'
let userStore = useUserStore(pinia)
export const isHasButton = (app: any) => {
// 获取对应的用户仓库
// 全局自定义指令
app.directive('has', {
// 代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次
// 指令的定义
mounted(el: any, options: any) {
// 自定义指令右侧的数值,如果在用户信息buttons数组当中没有
//从DOM树上干掉
if (!userStore.buttons.includes(options.value)) {
el.parentNode.removeChild(el)
}
},
})
}
2.在main.ts文件中注入该文件
// 引入仓库
import pinia from '@/store'
const app = createApp(App)
// 安装element-plus插件
app.use(ElementPlus, {
locale: zhCn, //国际化配置
})
app.use(pinia)
// 安装自定义插件
app.use(globalComponent)
// 注册模版路由
app.use(router)
// 引入路由鉴权文件
import './permission'
// 引入自定义指令文件
import { isHasButton } from './directive/has'
isHasButton(app)
// 将应用挂载到挂载点上
app.mount('#app')
总结
前端权限控制不仅仅是页面和路由的访问权限,还包括按钮、操作权限的细粒度控制。它与后端的权限体系相辅相成,前端负责用户体验的优化与增强,而后端则负责核心的权限认证与校验。在实际项目中,合理设计权限系统可以提高系统的安全性、灵活性和可维护性。通过接口、路由和按钮的权限控制机制,前端开发者可以构建一个更加安全、用户友好的应用系统。