权限管理是很多管理系统常见的需求,说起来这是一个比较简单的功能,但是每个人在实现的时候,往往都是以满足自己的眼前需求为动机,不太考虑整体的设计,导致使用不友好,拓展不方便,甚至代码层面也是千奇百怪,使用各种方式的都有。
遥想几年前,我还是实习生的时候,实现自己的第一个权限方面的需求,采用的是将用户的角色存储在localStorage里面,然后前端代码根据这个角色来判断隐藏菜单栏和一些按钮。后端压根没有权限表,只有角色表。
这样的做法现在来看是很不好的,最关键的是相当于将权限写死在前端了。后来,我做其他项目的时候,曾经采用前后端交互,然后前端在最开始的时候调用权限接口,然后前端再做了一大堆处理接口返回数组的逻辑。现在来看,这样做还是有些问题,把简单的事情复杂化。
其实,权限模块可以做的完全动态化,可拓展,可编辑,使用友好。最关键的是,下次再有后端/前端兄弟和你讨论如何实现权限功能,你可以按照本文介绍的大致设计告诉他,省了很多撕逼的时间。
本文前端以Vue为例子来进行说明,后端从字段和设计表方面来说明。
首先,我们在项目里配置好路由(这一步也可以是一部分前端提前配置,一部分取后端返回的数据渲染路由),这里我们就全部由前端配置好了。
{
path: '/',
name: 'layout',
redirect: 'dashboard',
component: () => import('@/views/layout/Layout'),
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard/index')
},
{
path: 'job',
component: () => import('@/views/task/index'),
children: [
{
path: '',
component: () => import('@/views/task/TaskList'),
},
{
path: 'create-task',
name: 'create-task',
component: () => import('@/views/task/TaskForm'),
}
]
},
{
path: 'config',
component: () => import('@/views/config/index'),
children: [
{
path: 'task-type',
component: () => import('@/views/config/taskType/index'),
},
{
path: 'service',
component: () => import('@/views/config/serviceConfig/index'),
},
{
path: 'auto-scaling',
component: () => import('@/views/config/autoScalingConfig/index')
},
{
path: 'handler',
component: () => import('@/views/config/handlerConfig/index')
},
{
path: 'schedule',
component: () => import('@/views/config/detectConfig/index')
},
{
path: 'template',
component: () => import('@/views/config/templateConfig/index'),
},
{
path: 'context-keys',
component: () => import('@/views/config/contextKeys/index')
},
{
path: 'task-alert',
component: () => import('@/views/config/taskAlert/index')
}
]
},
]
},
{
path: '/others',
component: () => import('@/views/others/index'),
children: [
{
path: 'release',
component: () => import('@/views/others/Release'),
}
]
},
{ path: '*', redirect: '/404', hidden: true },
{ path: '/404', component: () => import('@/views/error/404'), hidden: true },
{ path: '/401', component: () => import('@/views/error/401'), hidden: true },
{
path: '/login',
component: () => import('@/views/login/index'),
name: 'login',
},
我们要明白,路由配置到底是前端配置好,还是取接口返回数据进行渲染,并不是很Matter的问题,因为我们要看重的是菜单栏和用户输入路由进入的时候的权限。
然后,我们需要有一个菜单管理的页面,在菜单管理页面,新增菜单的时候,我们把菜单分为两部分,一部分是菜单栏的目录和菜单,也就是顶部或者侧边展示的菜单栏;一部分是后端接口(包括前端的增删改查按钮,也有其他的很多接口)。这样子就完全满足任意的需求了,因为后端的接口权限也在这里直接可以编辑配置。
新增页面的字段大概如下:
后端根据菜单排序来返回数组的顺序,前端直接根据接口返回的数组渲染菜单栏。
路由地址就是前端配置好的路由里面的path的值,前端的多语言也根据这个字段作为词典即可。
是否可见,我这里的设计是因为有些没有子菜单的菜单,但是它也许会有子页面(通过$router.push方法进入),我的判断是如果它有children数组而且children的类型为菜单的可是否见均为false的话,就意味着在菜单栏里它是没有子菜单的。
功能也就是后端接口(包括前端的每个页面的增删改查):
上级层级就是我们新增出来的一个个目录:
这个树形选择框是使用的vue-treeselect。挺好的一个vue工具。
这样子,后端就使用一张菜单表,就可以满足需求了。根据选择的层级目录ID,把它放到正确的children数组里,不用再去搞乱七八糟的权限表,权限模块表等等,本来问题就是这么简单。
菜单表的list如下:
这里的Job就是没有子菜单的菜单,但是它里面是有子页面的。
然后我们新建角色表,就可以给角色分配菜单和接口的权限了:
之后我们把角色表和具体的用户关联起来即可。这里,我们既可以分配菜单,也可以分配接口,同时,页面的具体按钮可以根据是否勾选了接口来决定是否展示。没有勾选的,后端不需要返回给前端。
这样的设计即清晰明了,实现起来也简单,而且完全动态化,可编辑,对管理员来说,交互也比较方便。
至此,后端的设计基本就结束了。我们完成了菜单管理,角色管理。现在前端需要考虑如何动态的渲染菜单栏,以及在用户输入地址的时候,如何判断他是否可以进入。如果不可以进入,我们应该跳转无权限页面。