樱町论坛的权限控制

樱町的权限控制是一种类似访问控制列表的东西,权限控制在樱町的核心中实现。为了实现较细的权限控制粒度,樱町使用了“权限项”作为权限控制的最基本单位,并辅以“作用域”来实现不同版面下的权限控制。

看图理解好说话:


权限定义

权限项

开发人员在实现新功能的时候,可以自己定义一个新的权限项,作为新功能的权限控制。比如,在完成“查看用户信息”这个功能的时候,可以定义一个名为“查看用户信息”的权限项。在用户将要查看用户信息的时候(如访问用户信息页面的时候),由开发人员调用樱町内核的接口,检查当前用户是否有“查看用户信息”这项权限,然后根据检查结果决定是输出用户信息,还是输出“禁止查看”的信息。

权限项是最基本的东西,在樱町论坛中,每一个非公告的操作都有一个对应的权限项。权限的取值有“未指派”、“允许”和“禁止”三种。“未授权”意味着用户没有这项权限,“允许”意味着用户有这项权限,而“禁止”意味着用户没有这项权限。即使在权限项的高优先级的作用域中已经为这个权限项指派为“允许”,但只要该权限项在某个作用域中被指派为“禁止”,那么用户就没有这个权限。

作用域

限项有4个作用域,分别是“论坛”、“版面”、“用户组”和“用户”,其优先级由低到高。分这几个作用域是因为,有些权限和帖子不相关,这些和帖子不相关的权限就可以在“论坛”作用域上定义。而那些和帖子相关的权限就应该在“版面”作用域上定义。同样,和用户组或用户相关的权限就在“用户组”或“用户”作用域上定义。“用户组”定义域定义的是一个用户组的权限,而“用户”定义域定义的是单个用户的权限。

为了实现更细的粒度,“用户组”和“用户”定义域又被各自分出了若干个子域,子域数量和版面数量相等,作为分版面权限控制,这样就可以在某个特定的版面指派权限了。

每一个权限项实际上在4个作用域上都有取值,只不过是把“未授权”这种状态看成是未定义。

权限计算

当开发人员调用樱町内核接口查询某项权限的时候,除了指定权限项外,还可以根据情况指定版面编号、用户组编号和用户编号。如果指定了版面编号,樱町内核就会使用所有4作用域上的权限值来计算最终权限。如果没有指定版面编号,樱町内核就不使用“版面”作用域上的权限值,而只使用其他3个作用域上的权限值来进行计算。如果开发人员没有指定用户组编号或用户编号,樱町内核就会使用当前用户的用户组和用户编号来取“用户组”和“用户”作用域上的权限项值。

由于“用户组”和“用户”可能有子域,因此,如果开发人员指定了版面编号的话,就还要取这两个作用域上对应子域上的权限值。这样最终取到的权限值就可能有6个。多出来的两个就是“用户组”和“用户”定义域的子域上的权限值。

取到各个作用域上的权限值后,就可以开始计算权限。权限的计算结果和权限项可取的值一样。如果存在值为“禁止”的权限值,则最终的权限值恒为“禁止”;否则,如果存在值为“允许”的权限值,则最终的权限值为“允许”;除此之外的其他情况,也就是所有的权限值都为“未指派”的情况,最终的权限值也将是“未指派”。

权限数据在数据库中的组织

表的结构

樱町使用了一个表来保存权限项的名字,每一个权限项就都有自己对应的编号了。另外,使用4张表来分别保存4个作用域上的权限,这4张表里面使用权限项对应的编号来记录权限指派情况。最简单的是“版面”作用域表,这个表里面只有4个字段,分别是权限项编号、指派掩码、版面编号和权限值。“用户组”和“用户”作用域表就分别比“版面”作用域表多了“用户组编号”和“用户编号”字段而已,而这两张表中的版面编号字段就成为了在分版面定义域上指派权限时的分版面编号,也就是“用户”或“用户组”定义域上的相应版面编号的子域。

在“用户”和“用户组”权限表上,当版面编号为NULL的时候,该条记录就是“用户”或“用户组”定义域上的权限值,也就是在所有版面上都有效的权限值。当版面编号不为NULL的时候,该条记录就是“用户”或“用户组”在对应版面编号的子域上的权限值。

这样定义以后,在“论坛”定义域表上,一个权限项名就唯一决定一个权限值。在“版面”定义域表上,一个权限项名和版面编号的组合就唯一确定一个权限值。在“用户”(或“用户组”)定义域表上,一个权限项名和版面编号以及用户(或用户组)编号的组合就唯一确定一个权限值。实际上在表中就把这些可以唯一确定权限值的组合定义为了主键,查询表的时候也必须提供被定义为主键的所有字段值进行查询。

权限值这个字段是布尔类型的,当为true的时候表示“允许”权限值,当为false的时候表示“禁止”权限值。在查询某个权限项的时候,如果没有查找到相关记录,就认为这个权限项值是“未指派”的。

掩码字段

设置这个字段,是为了记录该项权限是通过何种方式,或因为何种原因被指派的。每一个原因或者每一种方式对应掩码上的一个位,比如现在的樱町有下面两个指派方式和原因:

YT_PRIVILEGE_MASK_USER = 0x1; //管理员在后台手工指派的
YT_PRIVILEGE_MASK_BOARDMASTER = (0x1 << 1);  // 因为用户成为了版主,所以需要此项权限

当管理员将某人设为版主,樱町内核就会自动为此人授予相应的权限,此时的掩码是:

00000010  (二进制形式)

接下来,管理员又手工为这个用户分配了同样的权限,于是掩码变成:

00000011  (二进制形式)

这样就可以清楚地知道一个权限是为什么被指派的了。

如此麻烦地设置这样一个掩码当然是有原因的,如果不设置这样一个掩码的话,就会遇到下面这种情况:

出于一些原因,管理员给一个用户授予了允许“删除主题”的权限。过了一段时间,管理员又将此用户设为了版主,樱町内核会自动授予这个用户“删除主题”的权限,因为这是版主的基本权限(实际上这是通过另外一种可自定义的机制自动授予的权限)。又过了一段时间,管理员撤销了这个用户的版主职位,于是樱町内核也自动撤销了分配给这个用户的允许“删除主题”的权限。由于没有掩码,樱町内核不知道这个用户的允许“删除主题”的权限是不是之前由管理员手工指定的,于是用户不仅失去了作为版主所需的权限,同时还失去了管理员之前手工指派的权限。而按照正常的想法,撤销版主职位的时候,同时撤销的应该只有版主必需的权限,而不应该影响到自定义权限。

有了掩码之后,上述问题就可以得到很好地解决。管理员手工指定权限的时候,权限的掩码为00000001。接着,在管理员设置用户为版主,由樱町内核自动指派权限的时候,樱町内核首先将现在的掩码与YT_PRIVILEGE_MASK_BOARDMASTER做一个或操作,得到00000011,将这个结果作为新的掩码保存到数据库中。在管理员撤销用户的版主职位时,樱町内核会使用现在的掩码00000011上去掉YT_PRIVILEGE_MASK_USER所表示的位,得到00000010。由于掩码不为0,意味着这项权限还有用,不能删除,于是就只更新掩码,而不从数据库中删除这个权限项。

由于掩码的存在,指派权限的时候就不能随心所欲地指派权限。指派权限的时候,首先要判断此项权限是否已经由其他操作指派过,如果是,则继续判断想要指派的权限值是否和当前已经指派的权限值相等,如果相等才能指派,否则就不能指派。这是很显然的,如果之前已经通过其他方式指派了一个允许权限,现在又想将权限改为“禁止”,当然是不可能的,否则就会篡改到以前指派的权限。

这样的表结构就成为了所谓的“权限(访问)控制列表”

如果在数据表中有某项权限的记录,那么就可以知道这项权限已经被授予了“允许”或“禁止”的权限值。如果查询数据表而没有找到指定的权限项的记录,就知道这项权限还没有被指派,也就是处于“未指派”的状态。在这种状态下,相当于用户没有这项权限。

这样,权限表中一条一条的权限记录,排列下来就是所谓的“权限(访问)控制列表”了。有需要的权限,就指派之,数据表中就多一条记录。已经不需要了的权限,就撤销之,数据表中就少了一条记录。授权情况越复杂,数据表中的记录就越多,反之越少。一般来说一个论坛有几十项权限控制应该算正常,有上百条权限控制项应该就算多的了。再多的话,只能说这个论坛太庞大了,或者这个论坛在权限分配上根本就是有问题的。一个正常的论坛,权限项再怎么多,一般也不会上千吧,这种规模的记录数在数据库管理系统眼里看起来根本就是小菜一碟,不用担心因为指派了太多的权限而在数据库上造成瓶颈。

目前的不足

按照这样的权限设置方式,只能简单地实现允许访问和禁止访问,而无法实现根据用户属性来决定用户权限的功能。比如我想设定某个版面要求用户发贴达到10贴才能进入,这样的要求就无法完美地实现。

目前已经初步想到了一个方案,该方案为每一个权限项再增加一项属性,这个属性的值是一个布尔表达式,只有当布尔表达式的值为真时,权限项的值才有效。如果布尔表达式的值为假,即使已经为这个权限项指派了“允许”或“禁止”权限,也都认为是还没有为这个权限项指派权限值,也就是认为这个权限项的值是“未指派”。

布尔表达式中可以使用逻辑运算符号和简单的算术运算,樱町内核提供一些预定义变量,在计算布尔表达式的值的时候,自动将这些预定义变量替换为实际值。预定义变量可以是用户发贴数、用户注册时间、用户积分等数值属性。为了实现计算布尔表达式,看来还要写一个简单的编译器才行。直接让PHP来处理布尔表达式虽然也行,但是容易让用户提交有危险的代码,所以还是用自己的布尔表达式计算器好。另外,使用自己的布尔表达式计算器可以让用户不拘泥于PHP的语法,可以使用更贴近自然语言的表达式,甚至使用中文表达式,就像这样:

用户发贴数大于10且用户积分大于100

等同于

user_post_num > 10 && user_point > 100

但这只是初步想法而已,不知道这样的想法会不会存在什么致命缺陷。如果经过一段时间的考虑,没有发现这个想法有什么问题的话,再开始着手往樱町内核中加入这个权限控制方法。基于用户属性值的权限控制在论坛系统中是一项很基本的功能,这个功能樱町是不能不实现的。就算现在这个想法不能实施,也还是要想出其他的方法来实现基于用户属性值的权限控制的

举例说明

现在来按照樱町典型的一个权限分配,来说明如何在这样的权限分控制体系下指派权限。

樱町的权限是这样的,游客有除事务区以外所有板块的主题列表和帖子内容的权限,但是没有查看用户信息的权限。而注册用户拥有游客拥有的所有权限,另外还有查看用户信息的权限。而版面版主(版面版主属于“版主”用户组)除了拥有注册用户所有的权限以外,还有进入事务区板块的权限。

这只是樱町所有权限控制中的一部分,但是描述如何实现这一部分的权限已经基本上包括了论坛最典型的权限分配了,就以这样的权限设置来描述如何指派权限。

首先要说明,未登陆的用户属于“游客”用户组,已经登陆的用户属于非“注册用户”用户组,版主属于“版主”用户组。

  1. 在“板块”作用域上指派“查看帖子内容”、“查看主题列表”和“查看用户信息”权限,并设权限值为“允许”,这样所有的用户就都有了查看主题列表、查看帖子内容,以及查看用户信息的权限。注意,这时候游客也有了查看用户信息的权限,非版主用户也用有了进入事务区的权限;
  2. 在“用户组”定义域上为“游客”用户组指派“查看用户信息”的权限,权限值为“禁止”。这样游客就不能查看用户信息了,而注册用户和版主依旧能查看用户信息;
  3. 在“用户组”定义域的“事务区”这个板块的子域上,为“注册用户”和“游客”用户组指派“查看主题列表”和“查看帖子内容”的权限,权限值为“允许”。这样,非版主就没有进入事务区板块的权限了。

至此,权限指派就完成了,只指派了4个权限项(4条记录),就完成了上面描述的权限分配。当然一个普通的论坛还有其他的很通用的权限,这些通用的权限只要选择合适的定义域,指派合适的权限项及值,就能有效地完成权限分配。

好吧所谓有效地完成权限分配这只是我的猜想而已,我还没有试着指派过一个论坛所需要的所有基本的权限。不过感觉上,应该是比较有效的。一个普通的论坛,在常见情况下刚刚安装完毕后的默认权限,应该用不到几十个权限项就可以指派完了。至于实际是不是这样的,就要等我真正试着指派一次全论坛权限才能得到确切结果了。

本文发表于 技术向,并添加了 , , , 标记。保存永久链接到书签。

发表评论

电子邮件地址不会被公开。