0.背景和目的
后台管理系统一般都少不了登录权限控制,这里我们讨论以下如何实现权限控制。
1.主要概念
一个完整的权限控制模块涉及较多,一般包括用户管理、角色管理、资源管理(即菜单管理,当前系统总共有哪些菜单)。
用户
这个没什么好说的,就是你登录时候的账号,用户有自己的信息,比如姓名,账号密码,手机号等,用户登录之后会拿到用于区别用户身份的token,一般token会放在请求的接口的header里面,供后端来确定当前用户。
一个用户可以属于一个或者多个角色,用户会获得角色下的权限。
角色
角色可以理解为具有某些权限的一类用户,比如管理员角色,用于维护整个系统,具有最高权限,普通操作人员可能只具有某些业务的普通操作权限。
权限菜单
权限菜单可以理解为资源,比如当前有哪些菜单,哪些功能,功能下面有哪些按钮等。
权限有不同的粒度,比如有的系统相对简单只需要控制到具体页面即可,不再具体控制该页面下的具体按钮。
还有更进一步,除了菜单按钮之外,还可以控制到具体业务数据。
本篇只讨论控制到菜单和按钮级别。
2.核心流程
从流程上来说用户权限包括两部分:设定权限、拉取权限
设定权限
设定系统菜单
即当前系统的功能菜单、按钮管理,可以做成一个功能,如果系统简单也可以手动编辑数据库实现。
设定角色下的权限
由用户来勾选角色下包括的菜单、按钮。
设定用户所属角色
用户可以属于一个或几个角色,用户的权限是所属的角色最大集合
拉取权限
这里我们可以在全局状态管理(Vuex)设置一个状态标识(hasLoadAccess)和一个获取权限动作(loadAccess)
早期的时候动态生成路由并不现实,但当vue-router增加了addRouters方法之后,这一切变得简单了。
当用户拉取权限之后,根据得到的数据生成路由并addRouters和生成导航菜单。
3.服务端设计
数据库表设计
menu表
用于存储系统中需要权限验证的路由
id int 主键自增
name String 菜单功能名称
index String 标识,用户匹配本地组件
parentId int 菜单所属,如果是一级菜单值为0
isMenu int 是否是菜单
这里需要特别注意,因为有些路由属于菜单,有些路由不属于菜单,比如用户管理页面是需要显示在导航菜单中,但用户新增、用户编辑不应该出现在导航菜单中,这里我们都当作路由处理,有一个好处就是用户刷新可以停留在当前页面,逻辑相对简单,当然也可以不使用路由来处理这种编辑的页面,比如通过对话框、抽屉等,根据实际交互设计选择
icon String 结合iconfront 相应图标的类名
container String 容器组件标识,这里我们讨论二层路由,container为第一层组件
path String 路由,绝对路径
sort int 排序,用于控制菜单的顺序,可以手动修改
按钮表
存储系统中的所有按钮
id int 主键
uuid string 唯一标识
name String 按钮名称
parentId int 所属菜单的id
角色表
id int 主键id
name String 角色名称
sort int 顺序
角色权限表
id int 主键id
roleId int 角色Id
menuId int 菜单Id
用户表
id int 主键id
name String 用户姓名
username String 用户名
password String 加密码存储的密码
phone string 电话号码 不是必须有的字段,根据业务需求设定
用户角色关系表
id int 主键
userId int 用户id
roleId int 角色id
权限控制少不了后端的配合,除了对角色,用户等的增删改查外,还需要提供一个接口getAccess返回用户拥有的权限,返回结构大致如下:
{
code: 0,
data: [
{
id: 1
index: 'user-management',
name: '用户管理',
icon: 'icon icon-user',
refer: '/user-management',
children: [
{
index: '',
name: '',
refer: '',
isMenu: true
}
...
],
...
}
]
}
4.本地路由组织
存储本地所有路由组件
export default {
'console': import('path/console.vue'), // 根容器
'index':import('path/index.vue'), // 以键值对的形式,键为数据库中的index 根据index进行匹配,值为组件,这里使用import的方式
'user-management': import('path/index.vue')
}
5.权限获取
结合vue-router,我们可以在路由守卫的钩子函数beforeEach中执行动作,伪代码如下:
import router from 'vue-router'
import Store from 'vuex'
const whiteList = [ 'login' ] //不需要权限的路由
const token = Cookies.get('token')
...
router.beforeEach((to, from, next) => {
if(whiteList.indexOf(to.name) > -1) { // 跳转的是不需要登录状态的路由 直接放行
next()
} else if(!token) { // 需要权限 但是token不存在 直接跳转到登录页面
router.push({path: '/login'})
} else if(!Store.hasLoadAccess) { //需要权限 且token 但没有还没有获取权限 则开始获取权限
Strore.loadAccess()
} else { // 其余直接跳转到相应路由 这里一定要调用next 函数,不然不能跳转
next()
}
})
// 拉取权限动作,此处在vuex模块access中
export default {
state: {
hasLoadAccess: false,
accessTree: []
},
mutiation:{
setLoadStatus(state, status){
state.hasLoadAccess = status
}
}
action: {
async loadAccess(){
const res = await getAccess()
if(res.success){
// 处理路由
}
}
}
}
6.动态生成路由和导航菜单
生成路由
根据后台服务返回,生成路由数组,路由对应的组件从resourceMap中根据index进行匹配。
并不是所有路由都需要根据根据后端数据来生成,这里主要是指不需要用户权限验证的页面,如登录页面、找回密码等页面,这些路由可以写在前端初始路由里面。
生成菜单
生成菜单可以放在导航菜单的计算属性中,或者直接vuex 中的getter中
7.用户管理、角色管理、资源管理
此处为正常的增删改茶没有需要特别注意的内容
8.优化
按照以上逻辑即可实现权限控制,但是还有以下几个问题需要处理
1.当跳转到不是菜单对应的路由时,菜单缺少高亮状态
可以在menu表中增加refer字段,并存储到meta中,设定当前路由应该高亮的路由,比如用户管理页面的refer为‘/user-management’,即本身的path,用户新增、编辑页面的refer也为‘/user-management’
el-menu 组件的default-active设置为router.meta.refer
2.默认路由问题,一般登录后的默认主页是home页面,但如果在菜单设置的时候用户不勾选home页面如何处理。
有两种处理方式:
1、设置角色权限时home默认为选中状态且不允许取消选中。
2、允许不选择home菜单,当用户没有home页权限时,自动生成一个home页面,即不包含任何数据的欢迎页面。
3.后端接口过滤问题
这些过滤只是在前端对用户的动作进行限制,但是防君子不防小人,因为用户完全可以绕过前端的这些限制,直接利用postman请求接口,所以权限限制最终还要后端处理,进行用户验证。不过这是后端同学的工作,我们不做讨论。
4.资源管理问题
资源管理也就是我们的菜单、按钮的管理,如果产品在不断的迭代,需要频繁修改菜单,或者想把产品做的更晚上,需要一个资源管理功能,通过接口来处理菜单逻辑。
如果资源管理频率很低,也可以手动修改数据库,但需要注意的是手动修改menu表,不会处理角色的逻辑关系,当新增或者删除菜单时,需要重新保存角色权限,才会更新角色权限。
9.结语
以上是对vue项目中权限控制的一种简单实现,适合小型系统,如果对于多系统统一认证的系统,本篇文章的后端部分并不适用,前端部分可以借鉴参考。
原创不易,如果本篇文章对您有所帮助,还请点赞支持。如果您有疑问或者建议,请留言,我会在方便的时候做解答。