同一公司下多个产品共享用户的权限设计系统

本文所阐述的场景是:一家公司或一个部门抑或一个组织,旗下拥有多个产品,这些产品之间存在一定的联系,比如共享用户(可以统一登陆各个产品),但在不同产品间拥有不同权限,又比如,同一个资源(本文中的资源指系统中的内容对象,例如一条交易记录,例如一篇文章等等)在不同场景下对不同用户权限不同。

对于用户而言,他没有买(获得)一个产品的某个license,那他登陆产品后无法使用这个license所对应的功能。对于公司而言,如何管理好被授权用户(可能是一个公司购买了一个license,公司的所有员工都可以用)在不同产品下的权限,可不是一件简单的事。现在,我就来设计一个这样的系统。

对象设计

在这个系统里,会哪些基本的对象存在呢?它们又通过怎样的逻辑联系在一起呢?在这个系统里,我们需要明确它们之间最终在UI界面上如何展示。

产品

我们首先需要在这个系统中创建一个产品(注意,这个产品和我们线上运行的系统产品是两回事,这里的产品仅是一个代号,表示在我们的权限设计系统中,用以区分用户、权限将在哪个产品中生效),当然,系统中存在多少产品完全是公司产品线决定的,我们希望公司无论未来多少产品,这个系统都能满足产品权限控制的需要。

角色

这个产品的用户将被赋予某种角色。不同角色的用户得到的权利(entitlement)不同,因此,实际上对于用户而言,他拥有哪些权限,完全是因为他的角色而决定。

权利

权利 entitlement 是权限的落实点,用以告诉下游系统,某个功能是否可以工作。

很多情况下,我们会用“权限 permission”来表达这里的“权利 entitlement”。从词意上分析,“权限”是设计规则,用以限制能力;“权利”是指获得能力。这是我们日常设计中的一个知识缺陷,简单的说,权限是本文所阐述的整套系统的总称,而权利才是用以控制系统功能是否拥有工作能力的开关。我们日常常说的“权限”实际上是指本文中的“权利”。

设计权利非常复杂,因为它们需要和实际业务进行搭配。而且在最终的代码中有所体现。所以,设计权利体系时既要从业务本身出发,也要从开发者的角度出发,否则很容易搞错。下文我会详细阐述,如何进行 entitlement 的分类设计,从而帮助业务运营者和开发人员更好的使用。

后文我们会讨论,我们不一定非得把 entitlement 放在本系统中管理,因为不同的产品 entitlement 会有很多,而且数据格式也不一定,我们不一定要交付给产品 entitlement,而是只需要交付 role 给产品,由产品自己决定一个 role 需要获得哪些 entitlement。

当一个用户拥有多个角色的时候,他将得到这些角色权利的并集。当然,有的时候在一个产品里,某些权利是互斥的,也就是说同时拥有这两个权利会产生冲突,因此需要产品内部做进一步处理,但对于我们这套系统而言,并不需要考虑,我们要做的更通用一些。

许可证

许可证是用来卖的,一个产品上线之后,用户想使用它,需要得到提供服务的公司的授权,通过一个许可证来得到使用的权利。而一个 license 可能包含一个或多个角色(本质上,一个 license 就是一个 role set,即“角色组合”),拥有这个 license 的用户,就将得到这些角色对应权利的并集。

用户

我们大多数产品都尝试过开发自己都用户管理体系,但是,我们往往发现,越到后面,用户的权限越来越不好管理了。这是因为,一开始设计用户体系的时候,就没有把各个概念想清楚,导致想到一点加一点,到最后全部重新设计过。而本文一开始,就将用户纳入权限设计系统,避免产品设计一开始出问题。

我们要管理的这些用户,他们可能会购买license,也可能不会。有些产品可能并不需要license就能使用,只要这个用户存在于我们的系统里面。

另一种情况是,对于2B的产品而言,往往就有一个以整个公司(firm)为对象的管理单位,一个公司购买了一个license,那么它下面的所有用户都将拥有这个license都权利。还有一种设计是,一次性卖个这个公司特定数额都license,公司自己去分配,当这些license用完之后,你必须另外再买。这两种设计都是有的,不能只取其一。

因此,我将用户体系设计为“组织(公司或公益单位)-组(部门)-用户”。对于不同的产品而言,情况可能不同,有些产品仅卖给独立的用户,因此这些用户不存在三级组织关系。

管理员

管理员是登陆我们这个系统的用户,即可以把他们放置于上述用户管理体系内,也可以单独作为一个模块。如果把他们置于上述体系,那么相当于把我们这个系统也变成了我们这个系统里面的一个产品,这种自身包含自身的奇怪逻辑让系统设计非常难理解。因此,我们还是把管理员这个部分当作一个模块,管理员不是上述用户管理体系中的用户,而是本系统的用户,因此,这些管理员其实无法登陆本系统所管理的那些产品。

我们需要三层管理员:超级管理员,产品管理员,公司(组织、单位)管理员。超级管理员登陆之后,可以创建产品、公司、管理员,并且把管理员和产品、公司映射起来。产品管理员主要任务是管理自己的产品内的用户的权限怎么分配,比如那些公司购买了我的license,这些公司的角色,公司内的某些组的角色分配,甚至细到单个用户可以拥有哪些角色。而公司管理员则无法对产品相关进行操作,只能在有限的范围内为自己公司的组、用户进行角色分配和调整,而且,他只能看到自己购买的license所拥有的权限。

对象关系

上述这些对象,按照怎样的逻辑关联起来呢?它们是怎么相互作用,最终得以控制产品的权限的呢?

一个用户权限管理系统的设计示意图

理想的设计

上图展示了这个系统的理想设计。

  1. 系统上线之后,由超级管理员创建产品及其管理员。
  2. 随后,产品管理员登陆系统,创建这个产品的role、entitlement和license。
  3. 某个公司购买了该产品的 2 个 license:产品管理员在后台,创建该公司,并将这两个 license 授权给该公司。
  4. 该公司购买了该产品的 license 之后,获得一个管理员账号,这个管理员可以登陆该系统,对自己公司内的用户体系及权限分配进行管理。由于一个 license 的时候有限制,例如只能给 n 个人使用,因此,公司管理员要决定将这个 license 分配给哪些人。一个 group 表示这个 group 的人将同时继承该 group 的权利,但是必须注意,倘若这个 group 的人数超出 license 的使用人数,那么也会产生错误。
  5. 一个 license 是多个 role 的集合,该公司获得多个 license 之后,实际上获得了这些 role 的并集。公司管理员,可以更为详细的进行分配,比如,将某个 role 分配给某个 user。当然,license 里面的 role 也受到 license 中授权数量的限制。
  6. 一个单用户,只想购买其中某一个功能,产品管理员创建了一个新的 role,这个 role 在产品系统中仅开放了该功能,产品管理员创建了一个 license,且只包含这个 role,并卖给了该用户。

两个不同的产品之间需要共享用户和权限怎么办呢?比如说,某两个产品,其实同时集成了同一个功能,希望它们的用户在产品 A 中购买了 license,这些用户在产品 B 中照样还能用该功能。这种情况需要超级管理员在两个产品之间共享 role,这和共享 user 是一样的道理。在共享 role 的基础上,可以共享 license,技术上就是做一个软链,或者通过克隆复制一份,这两种方式都应该提供,因为有的时候确实只要做一个克隆就可以了,而有的时候,要同时编辑。

现实的考虑

但是在现实中,我们可能并不存在超级管理员这样的角色。我所在的公司通过一家第三方公司来售卖 license,当和一家公司签约后,由这家第三方公司通过 api 把这个 license 的售卖情况写入到我们的数据库中。所以,产品管理员其实没有创建 license 和 organization 的权利,但是他需要维护一个 license 对应哪些 role。

我们也没有在自己的系统里实现 entitlement,因为它太庞大来。理想化的设计是将 entitlement 实现在我们的系统里,一个产品通过 API,获取当前用户在当前产品下的所有权利,这样这个用户对产品的操作权限就一清二楚。但是我们需要考虑两个因素:1.一个产品的 entitlement 可能有上千个,而且这些 entitlement 都会被产品的源码使用,2.如果将 entitlement 放在我们这个系统,那么势必需要考虑和产品共用数据库,产品不可能还要通过 api 来获取这些 entitlement,这肯定就影响性能,所以肯定会直接从数据库中读取所有的 entitlement。基于这两点考虑,我们并不在本系统中实现 entitlement,而是在系统中提供一种嵌入方式,对于产品而言,仅仅需要知道一个用户都属于哪些 role,然后利用这些 role,去在产品自身的数据库中去找到这些 role 对应的 entitlement,也就是说,在我们的系统中,产品仅细化到 role 这个层级。对于 entitlement 这个层级,我们的系统提供一个 iframe 的嵌入方式,由产品自己的后台页面来进行控制。当然,如果我们不提供这个 iframe,其实也是可以的,对于产品而言,一个 role 对应拥有哪些 entitlement,应该都是已经固定的,在代码里面配置好表即可。

当然,还有一种情况,就是设计之初,系统允许管理员通过后台的配置界面,对某些功能进行开关操作。这种情况下,就必须将 entitlement 做成在线的,而非写死在代码中。当然,其实,这两部分可以同时运用,将在线的,和写死在代码中的并集之后使用,毕竟写死在代码中,性能比从数据库中读取快的多(当然,缓存在 redis 中比写死在代码里可能更快)。

权限分层

对于一个用户的权限,它往往不是由单一的角色信息决定的,而是由角色、许可证、业务逻辑等综合决定的。我这篇文章设计的是站在一个公司拥有多条产品线的层面去思考。但是,当我们回到具体的某个产品的开发团队的角度,也就是写业务 api 的开发同学的角度,去思考这个权限系统,就会遇到各种各样的麻烦。

这里提到的“权限分层”的概念,是基于将用户的权限看作“流”的基础上。当要得到一个用户是否对某个操作拥有权限时,要经过层层判断,而非直接从数据库里面取出一个值就够了。代码层面,要通过取出很多值,然后根据“层”的先后顺序,对该用户在每一层上的权限“流”进行拦截和判断,如果不符合条件,直接弹出,用户也就没有该权利,只有符合条件,才会进入下一层进行判断。

一个系统最终会有多少个权限控制流,这取决于有多少个增、删、查操作(接口)。对于一个系统而言,可以说,几乎每一个 api 都会需要鉴权,如果为了控制界面,还可能需要有一个(堆)接口,帮助客户端判断当前这个用户是否要显示某些东西。而更重要的是 api 接口,这些接口控制着用户可以看到的东西、可以进行的操作。但是,权限又不能直接反映在具体的某个字段上,它只能以背后“隐藏的手”的形式存在。

将成百上千的接口后面的权限判断,用分层的方式加以总结,会有不错的效果。但是,比较麻烦的是,在撰写每一个接口的时候,都必须要进行层层判断,完成这些判断之后,才能走正常的业务逻辑。有没有一种方式,可以在更高一层去做这些分层的判断,而业务代码就是业务代码,和权限判断分开?有一个概念叫“API 网关”,我知道有一个叫 apigee 的系统专门实现了这种网关,有兴趣可以了解一下。基于这种 API 网关的概念,我们可以从新考虑 API 后台开发的架构,即在路由前面再加一层,用来做权限的判断。

entitlement 的设计

实际上,权限系统就是上面的思路了。本节要解决如何去设计 entitlement 的系统管理。

分类方式

首先,我们要明确一点,entitlement 的设计是为了控制用户是否可以对某个功能进行使用。“使用”其实包含了两个东西:1)界面上可以看到和操作;2)可以与服务端接口实现数据读写。

因此,entitlement 的分类首先要分这两种:

  • 展示性权利
  • 数据性权利

但并不意味着在存储时要分两个地方存储,因为同一个 entitlement 可以在展示时被使用,也可以在数据读写时使用,因为它控制着某一个功能。这种分类给我们的建议是,entitlement 的输出方式不同。很显然,展示性权利需要输出到前端。一种建议是,在前端资源中提供一个 policies 的字段,每一个资源都有哪些权利,通过这个 policies 对象来了解,前端也可以通过这个字段来进行界面上交互的控制。

命名方式

一个 entitlement 的命名,建议采用层级式的链式命名规则。例如 sign.review.list 这个权利,表示“是否拥有签署审批的列表权限”,之所以这样设计,是因为我们还可以用 sign.review 来控制整个“签署审批”功能,在一定场合下,起到简洁的控制方式。

开发方式

我们在判断一个用户是否拥有对某个模块进行操作的权限时,我们不应该直接用用户的角色进行判断,虽然确实用户角色意味着权限,但是假如一个用户需要经过多重身份判断时,会怎样?用角色判断,就是一连串的逻辑效应,麻烦。而 entitlement 是已经通过角色判断的产出物,在开发时,应该用 entitlement 进行判断。

当然,实际开发中,单纯 entitlement 判断是不可能解决所有问题的,有的时候还需要 entitlement 和其他业务逻辑一起搭配判断才行。但是,总之,我们尽可能只用 entitlement 而不在业务逻辑判断中使用 role 进行判断。

user roles -> [ computing ] -> business entitlement

在计算部分,一个 entitlement 的计算可以写成一个封装的函数或方法,将来需要对 entitlement 的计算逻辑进行调整时,修改该函数即可。

多量的存取问题

entitlement 用 01 表示允许与不允许,用二进制转化为 62 进制进行存储,当 entitlement 的数量超过 256 时,将所有的 entitlement 按每个单位 256 个的方式进行分组,每组单独得到一个 62 进制数,并用英文逗号隔开,将字符串保存到数据库中,再采用 redis 将结果缓存起来,随时读取,读取时,通过把 62 进制转换为 2 进制得到想要的结果。

多选项问题

某一个 entitlement 可能有多个选项,0 代表不可读写,比如 1 代表可读,2 代表可读写。这种情况怎么办呢?

对单记录的权限问题

某一个后面新加进数据库的某条记录,一个用户想要对这个记录进行操作,必须要获得对该记录的对应权限。这种将权限细化到单条记录的情况怎么办?(这已经超出了本文权限设计系统的范畴。)

API 设计

最后,我们要为产品提供api,让产品可以通过这些api来获取某个用户的权利列表。可以说,本系统是一个用户管理系统和权限管理系统的并集。

首先要解决的是用户的注册、登陆、密码设置等问题。目标公司的产品,所有用户都通过这个系统来进行注册、登陆,因此,需要本系统提供完整都Oauth方案。

其次要解决用户信息都修改,也就是说在本系统自己提供都UI界面之外,其他产品,如果有权限,可以通过api进行用户操作,比如修改用户的基本信息,重设用户的role列表等。实际上,系统本身的UI界面,也是通过这些API来操作的,UI界面其实是本系统的一个app实例。

最后就是要能够根据登陆产品的用户,来获取这个用户的权利。如果一个用户登陆了某个产品,那么接下来,产品系统会通过api从本系统获取这个用户在该产品里面拥有哪些权限,获得这些信息之后,用户在产品内的界面现实、操作都会受到影响。

小结

这是一个common的用户权限管理系统设计,也就是说它没有考虑每个产品的特殊需要。为了让系统更具有扩展性,在开发这套系统时,应该在某些环节考虑留下hook,以方便日后在某些逻辑过程中新增功能。

当然,本文有一些地方并没有提及,比如对于一个产品而言,能够从本系统获取信息,也需要鉴权,可以通过appkey+secretkey的方式。这些细节其实在业界都是有成熟的解决方案,只需要去实现即可。

2018-03-04 3283

为价值买单

本文价值32.83RMB