简述
近几年随着 react、angular、vue 等前端框架兴起,前后端分离的架构迅速流行。但同时权限控制也带来了问题。
网上很多前、后端分离权限仅仅都仅仅在描述前端权限控制、且是较简单、固定的角色场景,满足不了我们用户、角色都是动态的场景。且仅仅前端进行权限控制并不是真正意义的权限控制,它只是减少页面结构暴露、增强用户体验的功效。
场景
系统为后台管理系统,包含了用户创建、用户登录、用户管理自己的资源。用户经常会新增、删除,也可以根据工作情况随时调整页面、功能权限,所以采用用户-角色-页面权限方案实现。
为什么不行:
1. 根据前端路由表显示左侧菜单,但 vue-router 的路由表主要为了组织代码,经常我们所需要的菜单并非一致。比如某个前端路由 a 子路由有 b、c,但菜单中我们想要直接一级菜单就显示 b、c 或者将 b、c 各放到其他菜单下。所以这种非常不灵活。
2. 一个路由是菜单还是页面?是否需要显示到菜单中?是否验证权限?哪个角色或者用户拥有权限?这些都需要写到前端路由里面,一旦有任何权限变动就要大量调整代码。
3. 如果权限写死在前端,那么角色或者用户必须已知且固定不变。比如页面 1 的 meta 增加属性标识可访问的角色为 a 和 b。
页面
一个页面即一个前端页面,比如首页、用户管理页、资源管理页等。
基本思路为:前端路由保持不变,数据库存储菜单结构、页面权限控制(可以直接做成一个页面来方便管理)等,前端根据数据库中的菜单结构和权限信息来渲染一个菜单出来并只显示其有权限的菜单,并在路由守卫中进行权限控制防止手动输入 path 越权打开页面。
1. 前端路由(vue-router)中需要正常创建页面及路由。
2. 数据库存储菜单结构和页面权限信息。
a. 菜单(目录、非内容页)可以自己创建,不必要求前端路由中有,因为这是指菜单的可视化的组织结构。
b. 页面(内容页)必须是前端路由中已有页面,因为这是用户需要访问的内容。
c. 菜单和页面组成上下级关系,一级可以是菜单也可以是内容页,内容页也可以放在菜单下,不可见的内容页也可以放在一个普通内容页下,这样理论(需要页面菜单样式支持)可以组成无限级菜单。面包屑导航也根据此层级递归查询得到。
d. 菜单和页面的基本属性包括 title(对应路由 title)、name(对应路由 name)、path(对应路由 path)、父级、类型(菜单/页面)、是否可见(左侧菜单栏是否显示:部分页面可能是页面内的链接进去)、是否需要验证权限(部分页面比如首页无需验证权限大家都可以进入)。
e. 不需要控制权限且不需要显示到左侧菜单的路由这里可以不进行管理,比如 404 页面等。
3. 前台打开后获取获取数据库的所有菜单、页面及结构,根据是否登录、是否需要验证权限等进行控制,或无权限跳转至登录页。
4. 用户登录成功后,再获取用户对应的的页面权限列表,使用上一步获得的所有页面、结构和用户拥有权限的列表渲染出一个菜单,只包含此用户拥有权限的,提升用户体检,避免显示大量用户不能访问的菜单影响使用和不必要的功能暴露。
5. 路由守卫中根据上一步获得的权限列表判断每个跳转,无权限可返回 404 或无权限页面,防止用户手动输入 path 越权访问。
页面管理:
页面编辑:
功能
部分功能有事需要单独控制权限,比如用户管理页面可能允许多个角色查看,但是其中的“创建用户”功能只允许某一个角色使用,那么仅仅使用页面权限是不够。所以需要细粒度的功能权限控制。
网上的方案都是说:根据资源控制增、删、改、查等等,比如针对用户就是用户的创建、修改、删除、查询等。但是在我的实际使用中发现并不切合实际,最起码对像我这种管理后台,资源并不单纯的增删改查,可能有其他地方的其他操作中也会对此用户资源造成影响,比如禁用、删除角色也要禁用、删除用户,那么这个权限到底属于角色的权限还是属于用户的权限,或者后台又改了,角色又影响了其他资源或者不再对用户进行操作,都会影响权限控制。
所以更合理的方法应该为将每个功能单独进行控制并和页面进行关联,且不限定必须是增、删、改、查四种,可以任意定制,只需要与前后端开发约定一个唯一的标识即可。
如上的例子中,用户管理页面下有用户各种功能,角色管理页面中也有个角色禁用、删除功能,可以分别定义标识为 role_disable、role_delete,如果拥有 role_delete 权限即可,即使没有 user_delete 权限,也可以直接删除用户,否则就不要给其 role_delete 权限。
用户登录后,从数据库获取其所拥有的的权限列表并存入 vuex,包含页面和功能对应关系,例如页面 name 为 user:{user: ['user_delete', 'user_query']},页面中根据删除按钮可以v-if="hasPermission('user_delete')"判断即可。
页面功能管理:
获取用户拥有的权限:
角色
一个角色类似于一个身份或岗位,每个角色有自己的权限范围。
- 一个角色可以拥有多个页面权限。
- 一个角色可以拥有多个功能权限。
角色管理:
角色分配权限:
用户
用户可以创建、删除,一个用户随时可能变更工作内容,或者身兼数职,所以可以为其分配一个或者多个角色,他拥有的角色的权限就是他的权限。此时已经可以打通权限前端的权限分配,用户-角色-页面权限、功能权限。
用户管理:
用户分配角色:
前端效果
前端页面菜单效果:
后端权限
传统前后端不分离的情况下,路由都在后端统一管理,简单的方法比如用户管理页面 /user/ 那么他里面使用的接口都使用 /user/add、/user/delete 等相同前缀,那么只要判断用户拥有 /user/ 权限就可以访问/user/*所有接口。
前后端分离后面临的问题:
接口
方案:1. 需要控制权限的接口进行上传管理(可以做成管理页面)
2. 每个页面和功能可以关联多个接口,比如用户页面关联了用户查询接口和用户编辑接口,用户删除功能关联用户删除接口
3. 后端对请求的路径进行判断,用户->角色->页面/功能->接口,拥有接口权限即允许访问
4. 前后端分团队开发,不容易一一对照,且前端有自己的路由(此路由受限于代码组织结构)等等,无法使用传统方式简单处理
5. 相同的接口可能会被前端多个页面多次利用。
接口管理:
页面关联接口、功能关联接口:
请求的接口无权限时:
接口后端权限控制
后端控制其实很简单,只要前面管理功能做好即可,基本逻辑为:
1. 用户访问接口。
2. 判断用户和当前 path,根据用户->角色->页面/功能->接口 得到当前用户有权限的接口列表与当前path相比。
3. 若无权限(某些接口只需要登录就能访问的,比如获取用户姓名信息的需要排除在外)则直接返回失败,前端全局捕获后给出无权限提示。
总结
1. 用户管理
a. 用户增删改查
b. 每个用户分配一个或多个角色
2. 角色管理
a. 角色增删改查
b. 每个角色分配一个或多个页面、功能授权
3. 页面管理
a. 页面增删改查
b. 标记页面上下级结构、是否内容页(需对应前端存在的路由页面)、是否可见、是否控制权限等等。
c. 前端菜单、面包屑等对用户可感知的内容根据此上下级结构等进行渲染,不必受限于前端代码中的路由
d. 前端路由根据此权限表进行权限控制
4. 接口权限控制
a. 接口管理录入需要控制权限的接口
b. 将接口分别关联到页面、功能
c. 拥有功能权限则拥有对应接口权限,拥有页面权限则拥有对应的权限
d. 只要通过任意页面和功能拥有接口的权限则可以访问此接口