系统权限设计 - 推荐方案

2,739 阅读9分钟

认真写文章,用心做分享。

个人网站:yasinshaw.com

公众号:xy的技术圈

在上篇文章《系统权限设计 - 基本概念和思路》中,介绍了我们在做权限设计的时候需要注意的一些点。其中有两点比较关键,这里再次提一下:

  • 粒度:粒度很难把握,推荐以一个基本的“业务操作”为粒度;
  • 区分Access与Validation:其中,Access与数据无关,可以在网关那一层就挡住;Validation与数据有关,可以在下游Service写代码来做。

下面将从后端到前端来介绍整个权限设计的推荐实践细节。

后端实现细节

分别从Access和Validation的实现角度来介绍。

Access怎么做?

Access就是一个个写死的权限。比如在Spring Security中,它是一个个字符串。你可以把它写死在Config文件里,也可以存在数据库里。

一般来讲,存在数据库里更灵活,如果配上一个管理界面,也比较容易管理。这里简单介绍一下存在数据库里如何做。以Spring Security为例,可以使用自定义的Bean拦截所有请求。在Bean里面可以取到request的URL等信息,然后去查数据库或session或解析JWT Token等方式取得当前用户拥有的权限,再去进行匹配。

1.png

Validation怎么做?

Validation需要验证数据当前的状态等信息是否满足条件。甚至有时候,不同的角色对同一个状态,也有不一样的权限。Validation在设计和使用的时候,可以考虑以下四个因素:多个角色如何判断权限、短路设计、消除角色、白名单 or 黑名单。下面分别详细介绍这几个因素,然后给出一个推荐的通用Validator代码实现。

多个角色如何判断权限

一般来说,在稍微复杂一点的权限管理需求中,一个人往往有多个角色。那如何判断这个人是否对当前这个操作有权限呢?

按照一般来逻辑来讲,当前用户只要有一个角色对这个操作有权限,我们就认为当前用户对这个操作有权限。

短路设计

因为Validation需要去查询数据。在微服务的环境下,它甚至有时候需要call其它API。前面提到,只要有一个角色对这个操作有权限,我们就可以认为当前这个用户对这个操作有权限。那后续的判断逻辑就可以不走了,程序做成短路设计,有利于减少数据查询和API调用,提升性能。

消除角色

我们在写Validation代码的时候,来自业务方的叙述,可能与角色相关。比如某写作平台,在发布文章后,作者不能再修改文章,但网站的编辑可以。我们用伪代码表示这个validation的逻辑:

2.png

这样我们就把“角色”写死到了代码里。假如以后有另一种角色也可以修改文章,比如网络安全审核员。那就需要改代码,重新发布。这样就很不灵活。

我们可以尝试消除代码中的“角色”,而是改成权限。比如,我们赋予editor这种角色一个叫edit_published_article的权限,这样我们的代码就可以写成这样:

3.png

这样的话,我们只需要把这个权限赋予给新加的角色,它就可以进行这个操作了。无需修改代码。

那什么时候不能消除角色呢?

但validation一定可以完全消除角色的吗?)不是的。如果你的系统业务,会把角色的id放到业务数据库里,就不能在validation中消除角色

比如我们在上一篇文章中举的例子:如果当前用户是老师,那他可以查看自己课程的试卷。如果是教务主任,可以查看当前年级的所有试卷。这个时候,需要根据不同的角色,去不同的表拿不同的数据。所以“角色”一定会写到validation代码中。这是无法避免的。

但是大多数业务,我们是可以消除角色的。消除角色带来的好处也显而易见,而唯一的缺点是会增加很多权限,使得管理权限变得复杂一些。通常是对应到枚举上,一个枚举的value就会对应一个权限。不过我们可以通过添加“权限组”的概念来解决这个问题,后文会介绍权限组。

通用Validator代码实现

下面给出一个基于Java代码的通用Validator实现及其用法。读者也可以根据自己的需要进行增强:

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为例的一个示例代码:

image.png

当然,不同团队可以根据自己的实际情况进行取舍和改进。

用户组与权限组

有时候我们可能会根据业务需求,对RBAC模型进行一定的增强。比如用户组、权限组等。

用户组

如果用户太多,对一个一个用户管理角色可能会比较困难。这个时候我们可以抽象出“用户组”的概念。相当于公司的“部门”。这样就可以对一组用户来管理角色,可以让管理更加方便。

权限组

在前面我们提到,有时候在Validation中,可以“消除角色”。这带来的代价就是会根据数据的状态创建不同的权限,使得权限增多。比如高中有3个年级,我们想分别对这三个年级有不同的权限控制,就得创建三个权限。

另一种情况是Access对API的关系。在上篇文章中,笔者推荐的是以“业务操作”为粒度。比如发朋友圈,假设有三个步骤:上传图片,获取当前位置,确认发布。我们其实只需要一个发朋友圈的Access,而不是三个Access。但这个Access其实对应的是三个API,而每个API又可能不止一个Access。比如上传文件,我们在聊天的时候也会用到这个API。所以Access与API是多对多的关系。

权限多了,就不容易管理。所以可以抽象出一个权限组的概念,来更好地管理权限。

当然了,增加用户组和权限组都会带来一定的复杂性,使现有的权限模型变得更加复杂。所以再次提醒大家,在做权限设计的时候一定要遵循“够用就行”的原则,切勿过度设计

以上两篇文章是笔者对权限系统设计的理解和总结。如果读者有任何疑惑的地方,或理解不一致的地方。欢迎留言讨论~

公众号.png