系统权限设计 - 推荐方案 - 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

比如在 SpringSecurity 中,它是一个个字符串。在 Bean 里面可以取到 request 的 URL 等信息,然后去查数据库或 session 或解析

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

个人网站:yasinshaw.com

公众号:xy 的技术圈

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

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

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

后端实现细节

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

Access 怎么做?

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

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

https://p6.toutiaoimg.com/origin/pgc-image/338185b7d4af4a11b5c910bec832a2f8?from=pc

Validation 怎么做?

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

多个角色如何判断权限

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

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

短路设计

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

消除角色

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

https://p6.toutiaoimg.com/origin/pgc-image/f1687d8a61b44919bb09babb25286703?from=pc

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

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

https://p6.toutiaoimg.com/origin/pgc-image/90abcd6cdfae4c67a778419d1c043614?from=pc

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

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

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

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

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

通用 Validator 代码实现

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

https://p6.toutiaoimg.com/origin/pgc-image/30aeeea0849a4aa49548700fd58d43ab?from=pc

前端实现细节

处于对系统安全性的要求,我们在后端是必须要做权限控制的。而前端有时候也需要做相应的权限控制,是希望能在 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 为例的一个示例代码:

https://p6.toutiaoimg.com/origin/pgc-image/af1455cf73c14345a48343188629e297?from=pc

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

用户组与权限组

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

用户组

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

权限组

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

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

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

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

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

https://p6.toutiaoimg.com/origin/pgc-image/e73782f532724ce2ab3a90aa92e026de?from=pc