Skip to content

Instantly share code, notes, and snippets.

@halfcoder
Last active June 13, 2022 06:55
Show Gist options
  • Save halfcoder/5964307 to your computer and use it in GitHub Desktop.
Save halfcoder/5964307 to your computer and use it in GitHub Desktop.
权限系统方案 - 给某个技术团队

有关于新的权限系统的设想

写在前面

以下内容的最初来源是我对于HFICampus的新的权限系统的设想,希望能在性能和权限控制的细粒度之间尽可能达到一个平衡。在与Steven交流之后,我将我的设想和与Steven的聊天记录整理,并写成了这份Markdown。

数据表设计

role:“角色”及其相关的权限设置的数据表

role,顾名思义,是用来设置用户“角色”的。为什么不说“组”呢,因为我希望这个“角色”是比“组的成员”更为通用的东西。

比如一个club中,我们可以分为:

  • 管理员(拥有全部权限,比如增删查改,而且不只是对于内容,对于“成员表”之类的也有这样的权限)
  • 版主(请原谅我使用这个名字)(拥有部分权限,比如对于内容的增删查改)
  • 成员(拥有更少的权限,比如对于内容增查改)

而且这样的分类对于course也是适用的(或许现在看起来不大适用,但是可以对区分进行修改,尽量适用,实在不行也可以设置特别的角色)。

对于每一个“角色”的权限设置,以json的形式予以保存,这样就方便扩展了~

“角色”(role)与“”用户组(group)的关系

这个当属Steven最关心的问题了~

其实,我并不介意将“角色”理解成为“用户组”~我之所以强调“角色”而不准备沿用现有的“用户组”的概念,主要是为了强调它在各个内容模型中的的通用性。

permission:这才是真正的权限的数据表

字段设计

含有uid,m(odule)type,m(odule)id,n(ode)type,n(ode)id,permission等字段:

  • uid是用户的id
  • mtype是指club/course甚至message等等之类的“内容模型”的“标识符”
  • mid是指club/course这些“内容模型”的id;
  • ntype是指blog、page、topic、question之类的“内容页面类型”(开始精确到了页面了,有木有~~~)
  • nid是指blog、page、topic、question之类的“内容页面”的id(精确到了页面,有木有!!!)
  • permission就是精确的权限控制了,不妨用lcurd-设定~
    • l指list
    • c指create
    • u指update
    • r指read
    • d指delete
    • -指无该位置上的权限

特别规定:uid、mid和nid中,0代表全部的意思。

提问时间~

为什么不用tablename 和itemid来确定每一个item?

ntype和nid其实就分别是tablename中的一部分和itemid

考虑现有的aca_page表中id为2,gid为14的记录,其在permission表中的对应的权限设置记录可以这样表示:(假设需要设置的uid为53,设置权限为lcurd){uid=53, mtype="course", mid=14, ntype="page", nid=2, permission="lcurd"}

但是为了实现更大的默认设定空间,我把mtype和ntype分开来。这样配合上面的“uid、mid和nid中,0代表全部的意思”的设定,就可以构造出大量的默认设定,以此来减少permission表中权限设置的记录数,减轻数据库压力。

结合上面的role的设置,即可为一个用户对于特定的内容模型和内容页面配置默认权限。比方说uid为53的用户对于id为14的course下的page都拥有lcurd的权限,那么可以只建立下述权限记录:{uid=53, mtype="course", mid=14, ntype="page", nid=0, permission="lcurd"}

实际环境中,精确到单个页面的权限设定其实可以说是非常少的,因为为少数页面单独配置权限实在是一件很吃力的事情,因此permission表中的权限记录可以说多以上述的默认权限设置为主。

还有个问题...我们是用uid还是用gid?

为什么是用uid那样不是挺费劲的么...group?

在表里面

那为什么不用组的说?这样不就更加节省查询时间?之前我设计的就是用组的

精确到用户的话貌似会记录很多重复数据

主要是整个系统最先接触到的用户系统的概念就是“用户”——用户的uid是最先取得的。而group还要一步才能取得~而且每一种动作的发起者都是单个“用户”。“组”更多是作为“这个用户能做什么”的一个标识~

那把组放到session里面减少查询次数?

例如,每次登陆我都把所有附属组抽出来...

而且这样的话更好地镶嵌到我们现有的用户系统里面

(好了,这个问题,注意看~)

首先要强调的是:permission表是这个构想中的权限系统中最核心的表,“权限验证操作”最终也是对于这个表中记录的读取以及判断。在这样的设定下,无论是role还是group,对于其权限的设置最终“同步”到permission表中,或者是以与permission表中同样的表现形式交由权限验证函数予以处理

那么,在role还是group的权限设置会针对用户同步到permission表中后,最初的“用户集合”的权限设置到底是以role来定义还是以group来区分,其实并不重要。

如果group的权限设置不会针对用户同步到permission表中,而是单独保存,那么就有如下问题:为了得到一个用户最终的权限设置,在拥有uid的情况下,需要先从user_sub表中取得用户所属的组,再取得用户所属组的权限设置,再予以整合。这个显然是性能较低而且难以编写的

但是,为了进一步减少数据库占用,能否将多个用户的相同的权限设置予以整合呢?请看下面的思考题~

思考题

  • 在上面的permission表的字段设计当中,uid、mid和nid都是integer形式,只能保存单个用户、内容模型和内容页面的id。那么,如果将这几个字段改为varchar甚至text类型,用“1,23,54,36”的形式保存多个id,不就可以达到整合相同权限设置的目的了吗?
好的,现在请从下列方面,思考上述方案的利弊:
*    数据库储存(占用空间大小)
*    SQL语句的设计?(包括增删查改各个方面)
*    权限验证函数的编写

缓存设计

我会告诉你permission表才是最大的缓存吗?

嗯,你没看错,permission表确实是最大的缓存。这个可不是我故意设计的,而是在设定完其它部分的功能之后,就成这样了。

为什么这么说?其实是因为大多数情况下,我们对于权限的设定,直接对象大多数都是role或者group,也就是“一组用户”。而为了提高这些设置的可重用性,我们会先将这些权限设置和role或者group的其它设置保存在一起,然后:

  • 如果已有用户属于这个role或者group,那么将这些权限设置应用到用户上,也就是修改用户在permission表中的权限记录
  • 如果有用户加入这个role或者group,那么将这些权限应用到这个新的“成员”上,也是修改用户在permission表中的权限记录
  • 如果有用户退出这个role或者group,那么根据这些权限设置清除用户在permission表中的权限记录

可见,permission表并非最初的权限设置的保存位置,而是经过处理之后的存储,其主要服务对象是权限验证函数。因此,可以称之为“缓存”。

而在少数情况下,我们确实会直接操作permission表。这些操作会在“函数设计”这一部分当中讲到。

基于Session的缓存

注意:这里的Session指PHP的原生Session,它是储存在服务器上的一个文本文件里面。而CodeIgniter的Session类使用客户端的Cookies来保存所谓的“Session”,在这种情况下,权限设置就不能够保存在Session里面,否则安全性将大打折扣。现在HFICampus项目的Session保存在数据库中,这更使得这样的缓存形式成为笑话,因为SQL查询并不能由于缓存的存在而显著减少,相反还会由于更多的类方法调用而占用CPU时间。不过这种缓存方式是我最初想到的,而且对于下面的缓存方案具有一定参考意义,所以还是列在这里。

更多有关于PHP原生Session的内容,请参阅http://www.php.net/manual/zh/book.session.php

PHP的原生Session其实相当于一个key/value的数据库,所以我们的存储形式自然也是key/value的形式,key中保存着用户id、内容模型类型、内容模型id、内容页面类型和内容页面id这些内容,value里面自然就是key中对应的数据记录的权限设置。而key中这一大串数据的保存方式是:用户id/内容模型类型/内容模型id/内容页面类型/内容页面id

示例:uid为53的用户对于id为14的course下的所有page的权限设置在Session中的记录的key为:53/course/14/page/0

正如上面所说,这种方式在HFICampus中难以应用,那么还有什么缓存方式吗?所幸SAE提供的KVDB可以视为一种补救的解决方案

基于KVDB的缓存

有关于SAE的KVDB的详细介绍,请参阅http://sae.sina.com.cn/?m=devcenter&catId=199

KVDB,顾名思义,也是一个key/value的数据库。而且它的实现比PHP原生的Session更好,用来做缓存还是不错的。缺点就是仅在SAE上提供。

这种方案的储存方式与基于Session的缓存方案一样。留给读者自己描述:)

函数设计

权限系统的函数整体上的呈现方式尚待讨论,不过我们可以看看个别函数的设计。

权限验证函数

权限验证函数主要有以下任务:

  • 从缓存中取得之前已经取得的权限设置
  • 构造SQL语句,从permission表中取得权限设置,并将其放入缓存中
  • 整合上面取得的权限设置,并进行判断
  • 返回“允许”(TRUE)或者“禁止”(FALSE)

权限记录应用的顺序

  1. uid确定、mtype确定、mid确定、ntype确定、nid确定的记录

  2. uid确定、mtype确定、mid确定、ntype确定、nid为0(全部)的记录

  3. uid确定、mtype确定、mid确定、ntype为all(全部)、nid为0(全部)的记录

  4. uid确定、mtype确定、mid为0(全部)、ntype确定、nid为0(全部)的记录

  5. uid确定、mtype确定、mid为0(全部)、ntype为all(全部)、nid为0(全部)的记录

  6. uid确定、mtype为all(全部)、mid为0(全部)、ntype为all(全部)、nid为0(全部)的记录

  7. uid为0(全部)、mtype确定、mid确定、ntype确定、nid确定的记录

  8. uid为0(全部)、mtype确定、mid确定、ntype确定、nid为0(全部)的记录

  9. uid为0(全部)、mtype确定、mid确定、ntype为all(全部)、nid为0(全部)的记录

  10. uid为0(全部)、mtype确定、mid为0(全部)、ntype确定、nid为0(全部)的记录

  11. uid为0(全部)、mtype确定、mid为0(全部)、ntype为all(全部)、nid为0(全部)的记录

  12. uid为0(全部)、mtype为all(全部)、mid为0(全部)、ntype为all(全部)、nid为0(全部)的记录

看晕了没?我写得还晕呢~

事实上,上述的顺序由以下规则推导得到:

  • 对于确定的数据记录的设置的优先级总是高于对于全部数据记录的默认设置
  • 不准出现ntype为all,而nid确定的情况。同理,不准出现mtype为all而mid确定的情况。这些情况的禁止应当在后台编程和添加权限记录函数、更新权限记录函数中得到体现。

读取缓存

缓存的使用请参见“缓存设计”部分,实际上是构造key来取得缓存当中的权限设置的value。

SQL语句的构造

这个其实是整个权限验证函数中最难的部分。好的SQL语句能够快速取得所要应用的那一条权限记录,还方便接下来的判断。

根据上述权限记录应用顺序的规定,SQL语句大概是要这么写的:

SELECT * FROM permission WHERE 
    (uid={uid} OR uid=0) AND 
    (mtype={mtype} OR mtype='all') AND
    (mid=(mid) OR mid=0) AND
    (ntype={ntype} OR ntype='all') AND
    (nid=(mid) OR nid=0)
 ORDER BY nid DESC, mid DESC, ntype DESC, mtype DESC, uid DESC

事实上,该SQL语句当中的WHERE部分总共会产生2^5=32种组合,这样的性能是不高的。根据从缓存中取得的结果可能可以减少一些条件进而减少总的可能数,但我尚未想到这样的方法。如果你有更好的SQL语句构造方式,欢迎在评论中提出来。

添加权限记录函数

添加权限记录函数主要有以下任务:

  • 检查现有的权限记录是否与即将添加的权限记录矛盾,并解决存在的矛盾
  • 添加权限记录
  • 更新缓存当中的权限记录

更改权限记录函数

更改权限记录函数主要有以下任务:

  • 检查现有的权限记录是否与更新的权限记录矛盾,并解决存在的矛盾
  • 更改权限记录
  • 更新缓存当中的权限记录

删除权限记录函数

删除权限记录函数主要有以下任务:

  • 删除权限记录
  • 更新缓存当中的权限记录

思考题

  • 在删除权限记录函数当中,没有像添加和更改这两个函数中的检查矛盾的任务。这样子会不会有什么问题?

函数应用

我个人希望权限验证函数放在控制器里显式调用,可以判断检查权限函数的返回值,通过就继续执行下去,否则直接redirect到一个显示错误的页面。错误信息的显示也可以多样化。

CodeIgniter的Hook(钩子)可以提供更加像样的权限检查时间,比如在控制器载入之前,但是由于这样会使得权限验证部分的扩展性不强,所以不建议采用这种方式。

修订记录

  • 2013年7月10日:第一版
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment