业务场景分析
假设我们在开发一个培训机构的客户关系管理系统,系统分客户管理、学员管理、教学管理3个大模块,每个模块大体功能如下:
客户管理
销售人员可以录入客户信息,对客户进行跟踪,为客户办理报名手续
销售人员可以修改自己录入的客户信息
客户信息不能删除
销售主管可以查看销售报表
学员管理
学员可以在线报名
学员可以查看自己的报名合同、学习有效期
学员可以在线提交作业、查看自己的成绩
教学管理
管理员可以创建新课程、班级
讲师可以创建上课纪录
讲师可以在线点名、批作业
从上面的需求中,我们至少提取出了5个角色,普通销售、销售主管、学员、讲师、管理员,他们能做的事情都是不一样的。
如何设计一套权限组件来实现对上面各种不同功能进行有效的权限控制呢?我们肯定不能LOW到为每个动作都一堆代码来控制权限,对吧?这些表面上看着各种不尽相同的功能,肯定是可以提取出一些相同的规律的,仔细分析,其实每个功能本质上都是一个个的动作,如果能把动作再抽象出具体权限条目,然后把这些权限条目再跟用户关联,每个用户进行这个动作,就检查他没有这个权限,不就实现权限的控制了么?由于这个系统是基于Web的B/S架构,我们可以把每个动作的构成提取成以下的元素。
一个动作 = 一条权限 = 一个url + 一种请求方法(get/post/put...) + 若干个请求参数
那我们接下来需要做的,就是把一条条的权限条目定义出来,然后跟用户关联上就可以了!
开发中需要的权限定义
什么是权限?
权限就是对软件系统中各种资源的访问和操作的控制!
什么是资源?
在软件系统中,数据库、内存、硬盘里数据都是资源,资源就是数据!
动作
资源本身是静态的,必须通过合适的动作对其进行访问和操作,我们说要控制权限,其实本质上是要对访问软件中各种数据资源的动作进行控制 。
动作又可以分为2种:
资源操作动作:访问和操作各种数据资源,比如访问数据库或文件里的数据。
业务逻辑事件动作:访问和操作的目的不是数据源本身,而是借助数据源而产生的一系列业务逻辑,比如批量往远程主机上上传一个文件,你需要从数据库中访问主机列表,但你真正要操作的是远程的主机,这个远程的主机,严格意义上来并不是你的数据资源,而是这个资源代表的实体。
权限授权
权限的使用者可以是具体的个人、亦可以是其他程序,这都没关系,我们可以把权限的授权主体,统称为用户,无论这个用户后面是具体的人,还是一个程序,对权限控制组件来讲,都不影响。
权限必然是需要分组的,把一组权限分成一个组,授权给特定的一些用户,分出来的这个组,就可以称为角色。
权限应该是可以叠加的!
权限组件的设计与代码实现
我们把权限组件的实现分3步,权限条目的定义,权限条目与用户的关联,权限组件与应用的结合。
权限条目的定义
我们前面讲过以下概念,现在需要做的,就是把我们系统中所有的需要控制的权限所对应的动作提取成一条条 url+请求方法+参数的集合就可以。
一个动作 = 一条权限 = 一个url + 一种请求方法(get/post/put...) + 若干个请求参数
以下是提取出来的几条权限
- perm_dic={
- 'crm_table_index':['table_index','GET',[],{},], #可以查看CRM APP里所有数据库表
- 'crm_table_list':['table_list','GET',[],{}], #可以查看每张表里所有的数据
- 'crm_table_list_view':['table_change','GET',[],{}],#可以访问表里每条数据的修改页
- 'crm_table_list_change':['table_change','POST',[],{}], #可以对表里的每条数据进行修改
- }
字典里的key是权限名,一会我们需要用过这些权名来跟用户进行关联。
后面values列表里第一个值如'table_index'是django中的url name,在这里必须相对的url name, 而不是绝对url路径,因为考虑到django url正则匹配的问题,搞绝对路径,不好控制。
values里第2个值是http请求方法。
values里第3个[]是要求这个请求中必须带有某些参数,但不限定对数的值是什么。
values里的第4个{}是要求这个请求中必须带有某些参数,并且限定所带的参数必须等于特定的值。
有的同学看了上面的几条权限定义后,提出疑问,说你这个权限的控制好像还是粗粒度的,比如我想控制用户只能访问客户表里的一条或多条特定的用户怎么办?
哈,这个问题很好,但很容易解决呀,只需要在[] or {}里指定参数就可呀,比如要求http请求参数中必须包括指定的参数,举个例子,我的客户表如下:
Customer表
里面的status字段是用来区分客户是否报名的,我现在的需求是,只允许用户访问客户来源为qq群且已报名的客户,你怎么控制?
通过分析我们得出,这个动作的url为
- http://127.0.0.1:9000/kingadmin/crm/customer/?source=qq&status=signed
客户来源参数是source,报名状态为status,那我的权限条目就可以配置成
- 'crm_table_list':['table_list','GET',[],{'source':'qq', 'status':'signed'}]
权限条目与用户的关联
我们并没有像其他权限系统一样把权限定义的代码写到了数据里了,也许是因为我懒,不想花时间去设计存放权限的表结构,but anyway,基于现有的设计,我们如何把权限条目与用户关联起来呢?
good news is 我们可以直接借用django自带的权限系统,大家都知道 django admin
自带了一个简单的权限组件,允许把用户在使用admin过程中控制到表级别的增删改查程度,但没办法对表里的某条数据控制权限,即要么允许访问整张表,要么不允许访问,实现不了只允许用户访问表中的特定数据的控制。
我们虽然没办法对通过自带的django admin 权限系统实现想要的权限控制,但是可以借用它的权限与用户的关联逻辑!自带的权限系统允许用户添加自定义权限条目,方式如下:
- class Task(models.Model):
- ...
- class Meta:
- permissions = (
- ("view_task", "Can see available tasks"),
- ("change_task_status", "Can change the status of tasks"),
- ("close_task", "Can remove a task by setting its status as closed"),
- ) 这样就添加了3条自定义权限的条目,然后 manage.py migrate 就可以在django自带的用户表里的permissions字段看到你刚添加的条目。
只要把刚添加的几条权限移动的右边的框里,那这个用户就相当于有相应的权限了!以后,你在代码里通过以下语句,就可以判定用户是否有相应的权限。
- user.has_perm('app.view_task')
看到这,有的同学还在蒙逼,这个自带的权限跟我们刚才自己定义的权限条目有半毛钱关系么?聪明的同学已经看出来了, 只要我们把刚才自己定义的perm_dic字典里的所有key在这个META类的permissions元组里。就相当于把用户和它可以操作的权限关联起来了!这就省掉了我们必须自己写权限与用户关联所需要的代码了。
权限组件与应用的结合
我们希望我们的权限组件是通用的,可插拔的,它一定要与具体的业务代码分离,以后可以轻松把这个组件移植到其他的项目里去,因此这里我们采用装饰器的模式,把权限的检查、控制封装在一个装饰器函数里,想对哪个Views进行权限控制,就只需要在这个views上加上装饰器就可以了。
- @check_permission
- def table_change(request,app_name,table_name,obj_id):
- .....
那这个@check_permission装饰器里干的事情,就是以下几步:
1.拿到用户请求的url+请求方法+参数到我们的的perm_dic里去一一匹配。
2.当匹配到了对应的权限条目后,就拿着这个条目所对应的权限名,和当前的用户,调用request.user.has_perm(权限名)。
3.如果request.user.has_perm(权限名)返回为True,就认为该用户有权限,直接放行,否则,则返回403页面!
权限检查代码
加入自定义权限
仔细按上面的步骤走下来,并玩了一会的同学,可能会发现一个问题,这个组件对有些权限是控制不到的,就是涉及到一些业务逻辑的权限,没办法控制, 比如我只允许用户访问自己创建的客户数据,这个你怎么控制?
通过控制用户的请求参数是没办法实现的,因为你获取到的request.user是个动态的值,你必须通过代码来判断这条数据是否是由当前请求用户创建的。类似的业务逻辑还有很多?你怎么搞?
仔细思考了10分钟,既然这里必须涉及到允许开发人员通过自定义一些业务逻辑代码来判断用户是否有权限的话,那我在我的权限组件里再提供一个权限自定义函数不就可以了,开发者可以把自定的权限逻辑写到函数里,我的权限组件自动调用这个函数,只要返回为True就认为有权限,就可以啦!
加入了自定义权限钩子的代码
权限配置条目
- 'crm_can_access_my_clients':['table_list','GET',[],
- {'perm_check':33,'arg2':'test'},
- custom_perm_logic.only_view_own_customers],
看最后面我们加入的only_view_own_customers就是开发人员自已加的权限控制逻辑,里面想怎么写就怎么写。
- def only_view_own_customers(request,*args,**kwargs):
- print('perm test',request,args,kwargs)
- consultant_id = request.GET.get('consultant')
- if consultant_id:
- consultant_id = int(consultant_id)
- print("consultant=1",type(consultant_id))
- if consultant_id == request.user.id:
- print("\033[31;1mchecking [%s]'s own customers, pass..\033[0m"% request.user)
- return True
- else:
- print("\033[31;1muser can only view his's own customer...\033[0m")
- return False
这样,万通且通用的权限框架就开发完毕了,权限的控制粒度,可粗可细、可深可浅,包君满意!以后要移植到其它django项目时,你唯一需要改的,就是配置好perm_dic里的权限条目!
作者:李杰
来源:51CTO