资源分组与URL设计

在开始之前,我们先观察一下我们已经非常熟悉的Linux的文件系统的路径是如何设计的。比如

/etc/rc.d/rc.local

Linux中从根目录开始,一层一层的检索想要获取的文件。根目录的名字是 “/”,他是固定不变的,在根目录后面是根目录中的文件名称,这里的名称是rc.d/,然后以此类推,直到最终要访问的文件。

现在我需要问你一个问题,文件的名字是保存在哪里的?很多人一直以为文件的名字是保存在文件中的,但是实际上,文件仅仅保存了文件的内容,跟文件相关的其他属性都没有保存在文件中。其实文件的名字是保存在它所在的目录中的,也就是说目录其实也是文件,只是比较特殊,目录的内容就是目录中包含的文件的信息。

实际上我们计算机到硬盘中去取文件的时候根本不需要知道文件的名字或者路径,而是使用文件在硬盘上保存的位置,这个位置信息也保存在目录中。也就是说,我们在取文件的时候,首先到固定的位置获取根目录的内容,根目录中保存着根目录下的文件的名字和保存位置的映射关系,Linux通过文件名字确定第二级目录的保存位置,然后再取第二级目录的位置,直到成功获取最终文件。

通常,一个文件只会保存在一个目录下,但是,实际上,一个文件是可以同时保存在多个目录下的,这样我们可以通过多个路径对同一个文件进行操作。

因为按照路径索引需要通过文件在目录中的名字获取它的保存位置,所以同一个目录下不能有两个同名的文件,这样我们才能在一个目录下唯一的确认一个文件。

但是按照目录索引还有一个很大的缺点,那就是因为不能直接获取文件的保存位置,想要检索某个文件就变得比较困难,如果你不知道你要查找的文件的完整路径,那么计算机只能通过依次在每个目录中检查你要检索的文件是否存在来帮助你了。而搜索文件的这个过程是十分漫长的。

但是,如果你记住了每个文件的保存位置,你可以直接通过这个保存位置来获取文件,这样可以跳过所有的目录层级,直接读取文件,这比使用路径访问文件还要快,但是这样会有更大的麻烦,没有人会记住他要是用的所有文件的保存位置。

但是,我们可以两种方式都使用,我们可以先使用层级目录的方式获得文件的保存位置,如果我们需要频繁的访问这个文件,我们可以把文件的位置记录下来,下次需要访问的时候直接使用这个保存位置操作文件。

理解了上面的内容对设计URL有哪些帮助呢?

  1. 文件的路径是文件名称的层级连接
  2. 文件的保存位置在硬盘上是唯一的

借助上面两条概念,我们在设计URL的时候也可以采取同样的措施,资源的名称不再是保存在文件的记录中,而是保存在父级资源组中,这样一个URL就变成了

/根资源组名称/次级资源组名称/…./资源名称

不过这样会有另外一个问题,文件系统中只有两种文件,目录和普通文件,而URL中,每一层资源组实际上都是一种资源,而不单单是为了组成索引。所以我们会需要看到URL就能知道每一层资源组具体对应着什么,比如

/某某公司/某某部门/某某办公室/某某人

我们需要在名字上直接说明资源组的类型,这样显然是浪费口舌,还有一种办法,就是把资源组先创建普通的组再添加到上级资源组中。比如

/公司/某某/部门/某某/办公室/某某/人/某某

这样,我们认为所有的公司在同一资源组中,而所有的公司在的组在根目录下。这样我们就可以轻松的了解资源组的性质,而且不需要为所有的资源组的名字加上资源组的类型。

不过还有一个问题,就是同一级目录中不能有两个重名的资源,如果我们在根目录下放了一个资源,他的名字就叫公司,这样和公司这个资源组会冲突。所以我们在起名字的时候就会更容易重名。不过我相信不会有人傻到犯这样的错误。

先将某种类型的资源放在这种类型的资源组成的组里再将这个组添加到父级资源组中,这样目录的层级就更加深了,就需要更多次的跳转了。如果目录的层级是固定的,比如部门一定是在公司下面的,那么我们可以通过他们的关系简化这个这种跳转。在层级是固定的情况下,如果你不在乎浏览者看到URL时会困惑的话,你也可以直接使用之前的方法,省略资源名称上的类型说明,直接使用

/某某/某某/某某/某某

这种索引方式。

但是最好的办法还是直接使用资源的保存位置来获取资源,在设计URL时,这个保存位置就是资源的ID,不过不同的资源往往是保存在不同的表中的,我们需要先说明资源所在的表,通常表名和资源类型是有对应关系的,也就是说

/人/ID

可以直接说明我们要找的是谁。如果你不是特别在意资源的层级关系,这样检索还好,但是如果你同时希望原来的层级检索方式还可以用,这样的表达方式就会有歧义。

我们是要找一个资源类型为人,保存位置为ID的资源,还是在根目录下找一个类型为人,名字为ID的资源组?

我们可以通过在根目录下创建两个不同名字的目录来区别

/Indirect/公司/某某/部门/某某/办公室/某某/人/某某

/Direct/人/ID

很显然上面两种路径表示的意思是不一样的。

如果你觉得Indirect和Direct会让你的访问者困惑,你也可以通过URL参数来指定索引

/公司/某某/部门/某某/办公室/某某/人/某某

/人?id=ID

后者的目录在根目录下只有一层,很明显不是按照层级查找的。

当然你也可以混合着两种模式

/公司/不确定/部门/某某/办公室/某某/人/某某?公司id=ID

或者

/公司/某某/部门/某某/办公室/某某/人?人id=ID

甚至

/人/某某?办公室id=办公室ID

当然,这样的索引即难写又难读,不推荐混合使用。

既然我们可以使用id来所有,那么使用名字搜索其实也不是不可以

/公司/某某/部门/某某/办公室/某某/人?name=Name

这样的表示方式和

/公司/某某/部门/某某/办公室/某某/人/某某

检索的结果一样,反而描述更加复杂了,似乎没有什么必要,但是我们可以这样检索

/公司/某某/人?name=Name

很明显,这样得到的应该是某某公司里所有名字为Name的人。原来的表达方式是没法完成这样的操作的。除了要实现这样的接口,你还必须把某某公司所有的人组成一个资源组并添加到某某公司下面,当然你的接口自动按照层级依次检索也没有什么问题,只是那样要执行的操作就太多了。

权限管理系统设计的一点理解

权限管理要回答的终极问题是某人对某资源是否有某权限。

围绕这个问题,最简单的方法就是直接记录某人对某资源是否有某权限,如果查询到有,那么答案就是是,如果没有那么答案就是否。

但是如果权限分的非常细致,可能的权限很多,那么某人对某资源的拥有的每一个权限都要记录一下,这样就要记录的行就太多了,要解决这个问题,可以对一些固定一起出现的权限进行合并,比如,要对某个资源进行改写,起码要有读取的权限,那么读和写可以合并为一个读写权限,这时在用户拥有写权限的时候就不需要额外记录一个读权限了。

通常我们会把对资源的权限抽象为增删改查,但是我们依然不能阻止有些情况下我们需要将权限更加细分,比如某人对某资源有读权限,同时有排序的权限,但是没有改写的权限,或者某人只能改写某资源的某一部分等等,这样细分下去,就会产生非常多的权限,如果在按照上面的方式对权限进行合并,排列组合就会产生更多的权限。这样就会使权限描述变得更加复杂,编程的时候要面对各种各样的权限。要解决这个问题,我们可以添加权限组的概念,首先梳理清楚某人对某资源存在的所有的基本权限。然后根据应用场景,要执行某一请求,则这个人必须拥有某些权限,然后将这些权限合并为权限组,编程的时候不需要管到底有哪些基本权限,只需要考虑在哪种应用场景下某人需要对某资源有哪个权限组。

当然,我们不希望用户可以拥有的权限那么复杂,我们先假设只需要记录几个权限,比如增删改查。但是我们依然需要为某个用户对应的每个资源进行记录,如果我们想要复制某人对应的多个资源的所有权限给另外一个人。比如某个人拥有编辑某些大量的文章的权限,现在给他安排了一个合作者,需要拷贝前者拥有的所有可以编辑的文章的权限给后者,我们就需要拷贝所有的记录给后者,这样需要进行大量的拷贝。如果这种情况时常发生,那么仅仅记录某个人对某个资源拥有某种权限的方法就会显得很繁琐。这种情况下,我们可以创建一个资源组,将某个人拥有的某些有共同特征的资源添加到一个组中,不再记录这个人对某个资源的权限,而是记录这个人对应这个组的权限。他对这个组有什么权限,就对组中的成员拥有什么权限。当我们给他安排一个合作者的时候,只需要添加后者对这个组拥有某些权限就可以了。

但是如果我们要复制权限的资源并不是很多,复制一次权限只需要添加几条记录的时候,我们不需要添加资源组。在这种情况下还会发生另外一种困难,如果很多人对某几个资源拥有共同的权限,比如我们希望100个人组成一个小组,他们共同维护几个资源,并且这个小组的成员很不稳定,经常会发生人员变动,那么每次发生人员变动都要修改变动的人对应这几个资源的权限记录。 或者这些人对应这几个资源的权限经常发生变化,每次发生变化都要修改每个人对应资源的权限记录。为了减少每次改动记录的条数,我们可以创建一个用户组,把这些人添加到一个用户组中,不记录每个人对应每个资源的权限,而是记录这个用户组对应这几个资源的权限。每次发生人员变动的时候只需要将发生变化的人添加到用户组或者移除用户组。

极端一些,如果有一大群人管理一大堆资源,而且他们对应每个资源的权限都是一样的,那么我们可以既使用资源组又使用用户组,当人员发生变化的时候就把人员添加或者移出用户组,当资源发生变化的时候就将资源添加或者移出资源组,当权限发生变化的时候我们直接修改用户组对应的资源组的权限就可以了。甚至我们还可以同时使用权限组,可以设计很多细分的权限,然后将这些权限分到各种各样的权限组,用户组和资源组之间的关系靠权限组关联。而某个人对应某个资源的权限使用某个用户对应某个资源的某个权限进行特殊处理。相信你看到这里一定觉得这个系统太复杂了,不容易维护,而且每次鉴权都要考虑很多种情况,某个人对应某个资源的某个权限,可以来自自身,可能来自用户组,可能来自资源组,可能来自权限组。

当然同时使用用户组和资源组的时候还有一种异常简单的情况,只需要判断某类人是否拥有某类资源的某个权限。比如所有的学生都拥有进入教室和在教室学习的权利,而不关心他们具体是谁,在哪个位置听课。如果一个系统中的所有权利赋予都是这种情况,我们就不需要单独记录某个人对应某个资源拥有某个权限了,而是直接将用户添加到用户组,资源添加到资源组中,然后直接关联用户组和资源组就可以了。

当然还有另外一种例外,那就是可以有用户组,也可以没有用户组,可以拥有资源组,也可以没有资源组,权利联系的双方具体是什么没有关系,只要他们符合某种条件就可以了,casbin就是这样的情况,只需要判断左右值是否符合条件,如果两边都符合条件,那么这个人对他请求的这个资源就拥有记录中的权限。然后通过符合条件的索引获得的资源一定是这个人拥有权限的资源。符合条件可能是因为他是某个用户组的成员,也可能是因为他就是条件中的唯一的一个人,而资源可能是因为它属于某个资源组,也可能是因为他就是条件中唯一的资源。

如果你要为某一类人创建对某一文件的某一权限,那么你就需要让代表这一类人的字符串有唯一的共同性。如果你要给某人某一类资源的某一权限,那么你就需要代表这一类资源的字符串有唯一的共同性。当然你也可以任性的记录某一个人对应某一个资源有某一权限,只要这个人的索引和这个资源的索引是唯一的就可以了,但是这样需要记录的权限条数可能会很多。

这样的设计特别适合用户和资源有层级关系的系统,比如某个公司使用某个文件系统。我们通过某部分的某某名字描述一个人,通过文件路径描述文件。那么部门就是用户组,目录就是资源组,这样用户的索引和文件的索引都是唯一的,而且表明了他们之间的关系。

当然没有层级关系的情况依然可以适用这套权限管理系统,只不过限制不在是在索引中,而是需要手动添加,比如,检索某个名字为name的文件,然后再url参数中添加作者为user,当对作者为user名字为name的文件拥有读权限的用户检索的时候,casbin就会放行,系统检索出所有名字为name,作者为user的文件返回,这样用户就获得了他想要的作者为user名字为name的文件,也因为他拥有对应的权限,他才顺利拿到了文件。

关于使用用户-资源组-权限组方式管理权限的分析

比如 A用户拥有1资源修改权限

权限管理的终极问题是 某个人是否有某个资源的某个权限
按照之前的理论 如果资源往往不是单独出现的,那么我们可以使用资源组的概念进行包装。

上面的描述就变成了

1资源属于 一资源组


A用户拥有一资源组修改1资源的权限 

这样的描述可以解除上面的描述

而A用户对一资源组不可能仅仅拥有几个权限,他可以拥有修改1资源的权限,也可能拥有修改2资源的权限,那么我们可以把这些权限合并到一个权限组中。

上面的描述就变成了


1资源属于一资源组
修改1资源的权限属于管理员权限组
A用户拥有一资源组管理员权限组

上面3条描述同样可以接触第一个答案。

然后某个人对某个资源是否有某个权限的问题就变成了

某资源是否属于某资源组

某权限是否属于某资源组

某人是否拥有某资源组的某权限组

这样虽然看起来更加繁琐了,但是在权限转让和资源变动的时候不需要像第一种描述那样,每个人对应每个资源的每个权限都要变动,只需要修改某人拥有的权限组或者某资源是否属于某资源组就可以了,谁变动修改谁,而不是一个环节变动其他环节都要跟着变。

这种用户-资源组-权限组的描述是实际上生活中很常见的。比如,小明是发财公司的网络管理员。小明就是用户,发财公司是资源组,网络管理员是权限组。

这个时候有人可能会觉得网络管理员是用户组而不是权限组,这种想法实际上是不对的。用户组的描述应该是 小明属于发财公司的网络管理员组。这两种描述的内涵是有区别的。