在上篇文章《系统权限设计 - 基本概念和思路》中,介绍了我们在做权限设计的时候需要注意的一些点。其中有两点比较关键,这里再次提一下:
- 粒度:粒度很难把握,推荐以一个基本的“业务操作”为粒度;
- 区分Access与Validation:其中,Access与数据无关,可以在网关那一层就挡住;Validation与数据有关,可以在下游Service写代码来做。
下面将从后端到前端来介绍整个权限设计的推荐实践细节。
后端实现细节
分别从Access和Validation的实现角度来介绍。
Access怎么做?
Access就是一个个写死的权限。比如在Spring Security中,它是一个个字符串。你可以把它写死在Config文件里,也可以存在数据库里。
一般来讲,存在数据库里更灵活,如果配上一个管理界面,也比较容易管理。这里简单介绍一下存在数据库里如何做。以Spring Security为例,可以使用自定义的Bean拦截所有请求。在Bean里面可以取到request的URL等信息,然后去查数据库或session或解析JWT Token等方式取得当前用户拥有的权限,再去进行匹配。
Validation怎么做?
Validation需要验证数据当前的状态等信息是否满足条件。甚至有时候,不同的角色对同一个状态,也有不一样的权限。Validation在设计和使用的时候,可以考虑以下四个因素:多个角色如何判断权限、短路设计、消除角色、白名单 or 黑名单。下面分别详细介绍这几个因素,然后给出一个推荐的通用Validator代码实现。
多个角色如何判断权限
一般来说,在稍微复杂一点的权限管理需求中,一个人往往有多个角色。那如何判断这个人是否对当前这个操作有权限呢?
按照一般来逻辑来讲,当前用户只要有一个角色对这个操作有权限,我们就认为当前用户对这个操作有权限。
短路设计
因为Validation需要去查询数据。在微服务的环境下,它甚至有时候需要call其它API。前面提到,只要有一个角色对这个操作有权限,我们就可以认为当前这个用户对这个操作有权限。那后续的判断逻辑就可以不走了,程序做成短路设计,有利于减少数据查询和API调用,提升性能。
消除角色
我们在写Validation代码的时候,来自业务方的叙述,可能与角色相关。比如某写作平台,在发布文章后,作者不能再修改文章,但网站的编辑可以。我们用伪代码表示这个validation的逻辑:
这样我们就把“角色”写死到了代码里。假如以后有另一种角色也可以修改文章,比如网络安全审核员。那就需要改代码,重新发布。这样就很不灵活。
我们可以尝试消除代码中的“角色”,而是改成权限。比如,我们赋予editor这种角色一个叫edit_published_article
的权限,这样我们的代码就可以写成这样:
这样的话,我们只需要把这个权限赋予给新加的角色,它就可以进行这个操作了。无需修改代码。
那什么时候不能消除角色呢?
但validation一定可以完全消除角色的吗?)不是的。如果你的系统业务,会把角色的id放到业务数据库里,就不能在validation中消除角色。
比如我们在上一篇文章中举的例子:如果当前用户是老师,那他可以查看自己课程的试卷。如果是教务主任,可以查看当前年级的所有试卷。这个时候,需要根据不同的角色,去不同的表拿不同的数据。所以“角色”一定会写到validation代码中。这是无法避免的。
但是大多数业务,我们是可以消除角色的。消除角色带来的好处也显而易见,而唯一的缺点是会增加很多权限,使得管理权限变得复杂一些。通常是对应到枚举上,一个枚举的value就会对应一个权限。不过我们可以通过添加“权限组”的概念来解决这个问题,后文会介绍权限组。
通用Validator代码实现
下面给出一个基于Java代码的通用Validator实现及其用法。读者也可以根据自己的需要进行增强:
前端实现细节
处于对系统安全性的要求,我们在后端是必须要做权限控制的。而前端有时候也需要做相应的权限控制,是希望能在UI上给用户更好的体验。比如,不该当前用户看到的页面,就不会出现在左边的导航栏。用户不能点击的按钮,就应该隐藏或者置灰。
页面权限控制
页面显示通常是比较粗粒度的UI控制了。如果角色及其权限相对稳定,可以死在前端配置里,这样开发成本比较低。
而如果角色及其权限容易变化,可以后端返回路由配置,这样就实现了用户,角色,路由的动态配置,全部统一管理。
具体实现细节大家可以参考掘金上的这篇文章:《如何优雅的在 vue 中添加权限控制》。
组件权限控制
组件权限控制是一种比较细粒度的UI控制。具体来讲,有两个方案:
- 前端写验证逻辑;
- 所有逻辑都在后端,后端返回Flag,前端根据这个Flag判断。
这两种方案各有优劣,下面我们来讨论一下。
前端写验证逻辑
如果是前端写验证逻辑,就是前端通过已有的数据,去判断组件是否可以显示或者可以操作。比如很多时候,某个按钮可不可以点击,是根据用户的角色,或者当前数据的状态来判断的。在一个表格页面,用户的角色和当前数据都是已知的。所以前端只需要写一个与后端一模一样的逻辑,就可以控制了。
这就会带来一个问题。比如我们删除一个数据,会根据这个数据的状态来做验权。后端肯定是需要写这个验证逻辑的,如果前端再写一份,那就会在前后端各自维护一段相同功能的逻辑。后期如果要修改逻辑的话,就需要前后端同时修改,造成代码维护上的不便。
另外一个问题是,如果前后端理解不一致,可能就会造成前端按钮看起来可以点击,但点击后,后端报了403错误。这可能是由于程序BUG,但如果前后端分离开来,就加大了在开发过程中,这种BUG产生的几率,降低开发效率。
还有一种情况是不适用于在前端写验证逻辑的。就是有些比较复杂的Validation,需要查其它数据库甚至是其它服务的数据,这种情况就不适合在前端做,不然可能要多Call好几个API。
后端返回Flag
如果是后端返回Flag,就可以解决上面提到的两个问题。这个时候,验证逻辑全部放到了后端,后端在“读”数据的时候,和真正进行业务操作“写”数据的时候,可以复用同一个Validation的逻辑。
后端返回Flag就是完美的解决方案吗?不是的。它同样会有两个问题。
第一个是对response结构的侵入。我们会在response里面加一个甚至是多个Flag,而这些Flag其实是跟业务数据是无关的。这里比较建议的是用偏业务的叫法来命名Flag,而不是偏前端UI的叫法。比如,叫canDeleteXXX
比叫showXXXButton
要好。
另一个问题是,有些操作可能只需要Access控制,不需要Validation。这个时候,其实后端也没有复用任何代码,因为进行“写”操作的时候,会在网关那一层通过Access验证权限,进来了没有走任何Validation。所以这种情况下,单纯为了加Flag,在读数据的时候去写逻辑判断Flag,反而不好。
推荐方案
对于一个操作的权限控制,通常有两种情况:
- 只需要Access,
- 需要Access + Validation
综上两种实现的比较,笔者推荐的方案是:后端返回的Flag只与Validation有关,前端写死的代码里只与Access有关。
下面是以Vue为例的一个示例代码:
当然,不同团队可以根据自己的实际情况进行取舍和改进。
用户组与权限组
有时候我们可能会根据业务需求,对RBAC模型进行一定的增强。比如用户组、权限组等。
用户组
如果用户太多,对一个一个用户管理角色可能会比较困难。这个时候我们可以抽象出“用户组”的概念。相当于公司的“部门”。这样就可以对一组用户来管理角色,可以让管理更加方便。
权限组
在前面我们提到,有时候在Validation中,可以“消除角色”。这带来的代价就是会根据数据的状态创建不同的权限,使得权限增多。比如高中有3个年级,我们想分别对这三个年级有不同的权限控制,就得创建三个权限。
另一种情况是Access对API的关系。在上篇文章中,笔者推荐的是以“业务操作”为粒度。比如发朋友圈,假设有三个步骤:上传图片,获取当前位置,确认发布。我们其实只需要一个发朋友圈的Access,而不是三个Access。但这个Access其实对应的是三个API,而每个API又可能不止一个Access。比如上传文件,我们在聊天的时候也会用到这个API。所以Access与API是多对多的关系。
权限多了,就不容易管理。所以可以抽象出一个权限组的概念,来更好地管理权限。
当然了,增加用户组和权限组都会带来一定的复杂性,使现有的权限模型变得更加复杂。所以再次提醒大家,在做权限设计的时候一定要遵循“够用就行”的原则,切勿过度设计。
以上两篇文章是笔者对权限系统设计的理解和总结。如果读者有任何疑惑的地方,或理解不一致的地方。欢迎留言讨论