西游之路——python全栈——如何开发通用且万能的权限框架组件
目录
一、本节内容
二、需求讨论
三、权限设计
四、代码设计
五、自定义权限钩子
一、业务场景分析 |
如何去实现将不同的权限分配给用户,假设我们在开发一个培训机构的 客户关系管理系统,系统分客户管理、学员管理、教学管理3个大模块,每个模块大体功能如下
客户管理
销售人员可以录入客户信息,对客户进行跟踪,为客户办理报名手续
销售人员可以修改自己录入的客户信息
客户信息不能删除
销售主管可以查看销售报表
学员管理
学员可以在线报名
学员可以查看自己的报名合同、学习有效期
学员可以在线提交作业 、查看自己的成绩
教学管理
管理员可以创建新课程、班级
讲师可以创建上课纪录
讲师可以在线点名、批作业
从上面的需求中, 我们至少提取出了5个角色,普通销售、销售主管、学员、讲师、管理员, 他们能做的事情都是不一样的
如何设计一套权限组件来实现对上面各种不同功能进行有效的权限控制呢?我们肯定不能LOW到为每个动作都一堆代码来控制权限对吧? 这些表面上看着各种不尽相同的功能,肯定是可以提取出一些相同的规律的,仔细分析,其实每个功能本质上都是一个个的动作,如果能把动作再抽象中具体权限条目,然后把这些权限条目 再跟用户关联,每个用户进行这个动作,就检查他没有这个权限,不就实现权限的控制了么?由于这个系统是基于WEB的B/S架构,我们可以把每个动作的构成 提取成以下的元素
一个动作 = 一条权限 = 一个url + 一种请求方法(get/post/put...) + 若干个请求参数 |
主要实现:将权限列表定义出来,与角色(用户组)相互关联就可以
二、开发中需要的权限定义 |
什么是权限?
权限 就是对 软件系统 中 各种资源 的 访问和操作的控制!
什么是资源?
在软件系统中,数据库、内存、硬盘里数据都是资源,资源就是数据!
动作
资源本身是静态的, 必须通过合适的动作对其进行访问和操作,我们说要控制权限,其实本质上是要对访问 软件中各种数据资源的动作进行控制
动作又可以分为2种:
资源操作动作:访问和操作各种数据资源,比如访问数据库或文件里的数据
业务逻辑事件动作:访问和操作的目的不是数据源本身,而是借助数据源而产生的一系列业务逻辑,比如批量往远程 主机上上传一个文件,你需要从数据库中访问主机列表,但你真正要操作的是远程的主机,这个远程的主机,严格意义上来并不是你的数据资源,而是这个资源代表的实体。
权限授权
- 权限的使用者可以是具体的个人、亦可以是其它程序, 这都没关系,我们可以把权限的授权主体,统称为用户, 无论这个用户后面是具体的人,还是一个程序,对权限控制组件来讲,都不影响 。
- 权限必然是需要分组的,把一些权限分成一个组,授权给特定的一些用户,分出来的这个组,就可以称为角色。
- 权限 应该是可以叠加的!
三、权限组件的设计与代码实现 |
我们把权限组件的实现分3步,权限条目的定义, 权限条目与用户的关联,权限组件与应用的结合
权限条目的定义
我们前面讲过以下概念, 现在需要做的,就是把我们系统中所有的需要控制的权限 所对应的动作 提取成 一条条 url+请求方法+参数的集合就可以
一个动作 = 一条权限 = 一个url + 一种请求方法(get/post/put...) + 若干个请求参数
以下是提取出来的几条权限
1 perm_dic={
2
3 'crm_table_index':['table_index','GET',[],{},], #可以查看CRM APP里所有数据库表
4 'crm_table_list':['table_list','GET',[],{}], #可以查看每张表里所有的数据
5 'crm_table_list_view':['table_change','GET',[],{}],#可以访问表里每条数据的修改页
6 'crm_table_list_change':['table_change','POST',[],{}], #可以对表里的每条数据进行修改
7
8 }
字典里的key是权限名, 一会我们需要用过这些权限名来跟用户进行关联
-
后面values列表里第一个值如'table_index'是django中的url name,在这里必须相对的url name, 而不是绝对url路径,因为考虑到django url正则匹配的问题,搞绝对路径,不好控制。
-
values里第2个值是http请求方法
-
values里第3个[]是要求这个请求中必须带有某些参数,但不限定对数的值是什么
-
values里的第4个{}是要求这个请求中必须带有某些参数,并且限定所带的参数必须等于特定的值
有的同学看了上面的几条权限定义后,提出疑问,说你这个权限的控制好像还是粗粒度的, 比如我想控制用户只能访问 客户 表里的 一条或多条特定的用户怎么办?
哈,这个问题很好,但很容易解决呀,只需要在[] or {}里指定参数就可呀,比如要求http请求参数中必须包括指定的参数,举个例子, 我的客户表如下:
1 class Customer(models.Model):
2 '''存储所有客户信息'''
3 #客户在咨询时,多是通过qq,所以这里就把qq号做为唯一标记客户的值,不能重复
4 qq = models.CharField(max_length=64,unique=True,help_text=u'QQ号必须唯一')
5 qq_name = models.CharField(u'QQ名称',max_length=64,blank=True,null=True)
6 #客户只要没报名,你没理由要求人家必须告诉你真实姓名及其它更多私人信息呀
7 name = models.CharField(u'姓名',max_length=32,blank=True,null=True)
8 sex_type = (('male',u'男'),('female',u'女'))
9 sex = models.CharField(u"性别",choices=sex_type,default='male',max_length=32)
10 birthday = models.DateField(u'出生日期',max_length=64,blank=True,null=True,help_text="格式yyyy-mm-dd")
11 phone = models.BigIntegerField(u'手机号',blank=True,null=True)
12 email = models.EmailField(u'常用邮箱',blank=True,null=True)
13 id_num = models.CharField(u'身份证号',blank=True,null=True,max_length=64)
14 source_type = (('qq',u"qq群"),
15 ('referral',u"内部转介绍"),
16 ('website',u"官方网站"),
17 ('baidu_ads',u"百度广告"),
18 ('qq_class',u"腾讯课堂"),
19 ('school_propaganda',u"高校宣讲"),
20 ('51cto',u"51cto"),
21 ('others',u"其它"),
22 )
23 #这个客户来源渠道是为了以后统计各渠道的客户量\成单量,先分类出来
24 source = models.CharField(u'客户来源',max_length=64, choices=source_type,default='qq')
25 #我们的很多新客户都是老学员转介绍来了,如果是转介绍的,就在这里纪录是谁介绍的他,前提这个介绍人必须是我们的老学员噢,要不然系统里找不到
26 referral_from = models.ForeignKey('self',verbose_name=u"转介绍自学员",help_text=u"若此客户是转介绍自内部学员,请在此处选择内部\学员姓名",blank=True,null=True,related_name="internal_referral")
27 #已开设的课程单独搞了张表,客户想咨询哪个课程,直接在这里关联就可以
28 course = models.ForeignKey("Course",verbose_name=u"咨询课程")
29 class_type_choices = (('online', u'网络班'),
30 ('offline_weekend', u'面授班(周末)',),
31 ('offline_fulltime', u'面授班(脱产)',),
32 )
33 class_type = models.CharField(u"班级类型",max_length=64,choices=class_type_choices)
34 customer_note = models.TextField(u"客户咨询内容详情",help_text=u"客户咨询的大概情况,客户个人信息备注等...")
35 work_status_choices = (('employed','在职'),('unemployed','无业'))
36 work_status = models.CharField(u"职业状态",choices=work_status_choices,max_length=32,default='employed')
37 company = models.CharField(u"目前就职公司",max_length=64,blank=True,null=True)
38 salary = models.CharField(u"当前薪资",max_length=64,blank=True,null=True)
39 status_choices = (('signed',u"已报名"),('unregistered',u"未报名"))
40 status = models.CharField(u"状态",choices=status_choices,max_length=64,default=u"unregistered",help_text=u"选择客户此时的状态")
41 #课程顾问很得要噢,每个招生老师录入自己的客户
42 consultant = models.ForeignKey("UserProfile",verbose_name=u"课程顾问")
43 date = models.DateField(u"咨询日期",auto_now_add=True)
44
45 def __str__(self):
46 return u"QQ:%s -- Name:%s" %(self.qq,self.name)
里面的status字段是用来区分客户是否报名的, 我现在的需求是,只允许 用户访问客户来源为qq群且 已报名的 客户,你怎么控制 ?
通过分析我们得出,这个动作的url为
1 http://127.0.0.1:9000/kingadmin/crm/customer/?source=qq&status=signed
客户来源参数是source,报名状态为status,那我的权限条目就可以配置成
1 'crm_table_list':['table_list','GET',[],{'source':'qq', 'status':'signed'}]
权限条目与用户的关联
我们并没有像其它权限系统一样把权限定义的代码写到了数据库里了,也许是因为我懒,不想花时间去设计存放权限的表结构,but anyway,基于现有的设计 ,我们如何把权限条目与 用户关联起来呢?
good news is 我们可以直接借用django自带的权限系统 ,大家都知道 django admin 自带了一个简单的权限组件,允许把用户在使用admin过程中控制到表级别的增删改查程度,但没办法对表里的某条数据控制权限,即要么允许访问整张表,要么不允许访问,实现不了只允许用户访问表中的特定数据的控制。
我们虽然没办法对通过自带的django admin 权限系统实现想要的权限控制,但是可以借用它的 权限 与用户的关联 逻辑!自带的权限系统允许用户添加自定义权限条目,方式如下
1 class Task(models.Model):
2 ...
3 class Meta:
4 permissions = (
5 ("view_task", "Can see available tasks"),
6 ("change_task_status", "Can change the status of tasks"),
7 ("close_task", "Can remove a task by setting its status as closed"),
8 )
这样就添加了3条自定义权限的条目, 然后 manage.py migrate 就可以在django自带的用户表里的permissions字段看到你刚添加的条目
只要把刚添加 的几条权限 移动的右边的框里,那这个用户就相当于有相应的权限 了!以后,你在代码里通过以下语句,就可以判定用户是否有相应的权限。
1 user.has_perm('app.view_task')
看到这,有的同学还在蒙逼,这个自带的权限跟我们刚才自己定义的权限条目有半毛钱关系么?聪明的同学已经看出来了, 只要我们把刚才自己定义的perm_dic字典里的所有key在这个META类的permissions元组里。就相当于把用户和它可以操作的权限关联起来了!这就省掉了我们必须自己写权限与用户关联所需要的代码了。
权限组件与应用的结合
我们希望我们的权限组件是通用的,可插拔的,它一定要与具体的业务代码分离,以后可以轻松把这个组件移植到其它的项目里去,因此这里我们采用装饰器的模式,把权限的检查、控制封装在一个装饰器函数里,想对哪个Views进行权限控制,就只需要在这个views上加上装饰器就可以了
1 @check_permission 2 def table_change(request,app_name,table_name,obj_id): 3 .....
那这个@check_permission装饰器里干的事情就是以下几步:
- 拿到用户请求的url+请求方法+参数,判断用户是否已登录user.is_authenticated后,再
到我们的的perm_dic里去一一匹配 - 当匹配到了对应的权限条目后,就拿着这个条目所对应的权限名,和当前的用户, 调用request.user.has_perm(权限名)
- 如果request.user.has_perm(权限名)返回为True,就认为该用户有权限 ,直接放行,否则,则返回403页面!
1 from django.shortcuts import render,redirect
2 from django.urls import resolve
3 from kingadmin.permission_dict import perm_dict
4 from django.conf import settings
5
6 def perm_check(*args,**kwargs):
7 """
8 1、获取当前请求url
9 2、把url解析成url_name
10 3、判断用户是否已登录user.is_authenticated
11 4、拿uri_name到permission_dict中取匹配,匹配时要包含请求方式和参数
12 5、拿到匹配到权限的key,调用has_perm(key)
13 :param args:
14 :param kwargs:
15 :return:
16 """
17 request = args[0]
18 resolve_url_obj = resolve(request.path)
19 current_url_name = resolve_url_obj.url_name # 当前请求url_name
20 # match_flag = false
21 match_key = None
22 # 是否通过登录验证
23 if request.user.is_authenticated is False:
24 return redirect(settings.LOGIN_URL)
25 result_matched = []
26 for permission_key,permission_val in perm_dict.items():
27 perm_url_name = permission_val[0]
28 perm_request_method = permission_val[1]
29 perm_args = permission_val[2]
30 perm_kwargs = permission_val[3]
31
32 # 通过长度检测是否有方法
33 perm_hook_func = permission_val[4] if len(permission_val) > 4 else None
34
35 if current_url_name == perm_url_name: # matchs current request url
36 if request.method == perm_request_method: # match request method
37 # 反射获取request.GET或request.POST
38 request_method_func = getattr(request, perm_request_method)
39 args_matched = False
40 # 逐个匹配参数,perm_dictd的args参数必须包含在请求参数里
41 for item in perm_args:
42 if request_method_func.get(item,None): # request 字典中有此参数
43 args_matched = True
44 else:
45 args_matched = False
46 break # 主要有一个perm_dic中的参数匹配不上request中的参数,则判定为假,退出循环
47 else:
48 args_matched = True
49
50 # 引用
51 kwargs_matched = False
52 # 匹配特定值的参数,循环kwargs,perm_dictd的kwargs参数必须包含在请求参数里
53 for k,v in perm_kwargs.items():
54 if request_method_func.get(k,None) == str(v): # str防止perm_dict中有整数,因为前端数据传输str
55 kwargs_matched = True
56 else:
57 kwargs_matched = False
58 break
59 else:
60 kwargs_matched = True
61
62 # 开始匹配自定义权限钩子函数
63 hook_func_macthed = True
64 if perm_hook_func:
65 hook_func_macthed = perm_hook_func(request)
66
67 result_matched = [args_matched,kwargs_matched,hook_func_macthed]
68 if all(result_matched): # 两个同时为真
69 match_key = permission_key
70 break
71 if all(result_matched):
72 app_name,*perm_name = match_key.split('_') # '*'的使用如:a,*b='crm_table_index' => a='crm' b=['table','index']
73 perm_obj = "%s.%s" %(app_name,match_key)
74 if request.user.has_perm(perm_obj):
75 print('当前用户有此权限')
76 return True
77 else:
78 print('perm_dict中配置有该权限,当前用户没有该权限')
79 return False
80 print('perm_dict中没有配置该权限,当前用户没有该权限')
81 return False
82
83
84 # 装饰器
85 def check_permission(func):
86 def inner(*args,**kwargs):
87 if not perm_check(*args,**kwargs):
88 request = args[0] # 传入的request
89 return render(request, 'kingadmin/page_403.html')
90 return func(*args,**kwargs)
91 return inner
补充(https://www.cnblogs.com/ssyfj/p/9135479.html):
定义权限列表文件,将权限列表定义。设置为装饰器,根据每个用户访问的url去反解,获取到对应的权限列表名,从而去数据库中获取,使用has_prem获取是否拥有权限。从而在用户访问url时进行判断
permission_list.py(这部分最好是放入数据库中,可以改进)
1 from kingadmin import permission_hook 2 3 perm_dict = { 4 'repository_table_obj_list':['table_obj_list',"GET",[],{},permission_hook.view_my_own_customers], 5 #第一个是url_name,第二个是访问方式,第三个是访问参数必须有的字段,第四个是字段中必须是指定的值,第五个是钩子函数,是对第三,第四参数的补充,实现动态验证 6 'repository_table_obj_change_view':['table_obj_change',"GET",[],{}], 7 'repository_table_obj_change': ['table_obj_change', "POST", [], {}], 8 'repository_table_obj_add_view': ['table_obj_add', "GET", [], {}], 9 'repository_table_obj_add': ['table_obj_add', "POST", [], {}], 10 'repository_table_obj_delete_view': ['table_obj_delete', "GET", [], {}], 11 'repository_table_obj_delete': ['table_obj_delete', "POST", [], {}], 12 }
==================
1 from django.conf.urls import url 2 from kingadmin import views 3 4 urlpatterns = [ 5 url(r"^login.html$",views.acc_login), 6 url(r"^logout.html$", views.acc_logout,name="logout"), 7 url(r"^$",views.app_index), 8 url(r"^(\w+)/(\w+)/$",views.table_obj_list,name="table_obj_list"), 9 url(r"^(\w+)/(\w+)/(\d+)/change/$", views.table_obj_change, name="table_obj_change"), 10 url(r"^(\w+)/(\w+)/add/$", views.table_obj_add, name="table_obj_add"), 11 url(r"^(\w+)/(\w+)/(\d+)/delete/$", views.table_obj_delete, name="table_obj_delete"), 12 ] 13 14 urls文件,可以知道对应的url_name
resolve方法可以翻转获取url的数据
permission文件,用于生成装饰器,验证权限列表
权限检查代码(已加入钩子)总结:
1 def perm_check(*args,**kwargs): 2 1.获取当前请求的url,使用resolve解析获取url_name 3 2.匹配用户是否登录,使用user.is_authenticated方法 4 3.使用url_name去权限列表permission_list文件中的权限列表中去匹配权限项 5 4.将权限项解析分为,per_url_name(权限url_name),per_method (url访问方法),per_args (获取的参数名),per_kargs (获取的参数值,字典),per_hook_name (获取的权限钩子函数) 6 5.验证了上面的几部分,获取了权限名,然后去数据库中获取当前用户是否拥有该权限,使用user.has_perm(权限名<注意:权限名是由数据表应用加上权限名>)
四、加入自定义权限 |
仔细按上面的步骤走下来,并玩了一会的同学,可能会发现一个问题,这个组件对有些权限是控制不到的, 就是涉及到一些业务逻辑的权限,没办法控制 , 比如 我只允许 用户访问自己创建的客户数据,这个你怎么控制?
通过控制 用户的请求参数 是没办法实现的, 因为你获取到的request.user是个动态的值,你必须通过代码来判断 这条数据 是否是由当前请求用户 创建的。 类似的业务逻辑还有很多?你怎么搞?
仔细思考了10分钟,即然这里必须涉及到必须允许开发人员通过自定义一些业务逻辑代码来判断用户是否有权限的话,那我在我的权限组件里再提供一个权限自定义函数不就可以了,开发者可以把自定的权限逻辑写到函数里,我的权限组件 自动调用这个函数,只要返回为True就认为有权限,就可以啦!
1 from django.shortcuts import render,redirect
2 from django.urls import resolve
3 from kingadmin.permission_dict import perm_dict
4 from django.conf import settings
5
6 def perm_check(*args,**kwargs):
7 """
8 1、获取当前请求url
9 2、把url解析成url_name
10 3、判断用户是否已登录user.is_authenticated
11 4、拿uri_name到permission_dict中取匹配,匹配时要包含请求方式和参数
12 5、拿到匹配到权限的key,调用has_perm(key)
13 :param args:
14 :param kwargs:
15 :return:
16 """
17 request = args[0]
18 resolve_url_obj = resolve(request.path)
19 current_url_name = resolve_url_obj.url_name # 当前请求url_name
20 # match_flag = false
21 match_key = None
22 # 是否通过登录验证
23 if request.user.is_authenticated is False:
24 return redirect(settings.LOGIN_URL)
25 result_matched = []
26 for permission_key,permission_val in perm_dict.items():
27 perm_url_name = permission_val[0]
28 perm_request_method = permission_val[1]
29 perm_args = permission_val[2]
30 perm_kwargs = permission_val[3]
31
32 # 通过长度检测是否有方法
33 perm_hook_func = permission_val[4] if len(permission_val) > 4 else None
34
35 if current_url_name == perm_url_name: # matchs current request url
36 if request.method == perm_request_method: # match request method
37 # 反射获取request.GET或request.POST
38 request_method_func = getattr(request, perm_request_method)
39 args_matched = False
40 # 逐个匹配参数,perm_dictd的args参数必须包含在请求参数里
41 for item in perm_args:
42 if request_method_func.get(item,None): # request 字典中有此参数
43 args_matched = True
44 else:
45 args_matched = False
46 break # 主要有一个perm_dic中的参数匹配不上request中的参数,则判定为假,退出循环
47 else:
48 args_matched = True
49
50 # 引用
51 kwargs_matched = False
52 # 匹配特定值的参数,循环kwargs,perm_dictd的kwargs参数必须包含在请求参数里
53 for k,v in perm_kwargs.items():
54 if request_method_func.get(k,None) == str(v): # str防止perm_dict中有整数,因为前端数据传输str
55 kwargs_matched = True
56 else:
57 kwargs_matched = False
58 break
59 else:
60 kwargs_matched = True
61
62 # 开始匹配自定义权限钩子函数
63 hook_func_macthed = True
64 if perm_hook_func:
65 hook_func_macthed = perm_hook_func(request)
66
67 result_matched = [args_matched,kwargs_matched,hook_func_macthed]
68 if all(result_matched): # 两个同时为真
69 match_key = permission_key
70 break
71 if all(result_matched):
72 app_name,*perm_name = match_key.split('_') # '*'的使用如:a,*b='crm_table_index' => a='crm' b=['table','index']
73 perm_obj = "%s.%s" %(app_name,match_key)
74 if request.user.has_perm(perm_obj):
75 print('当前用户有此权限')
76 return True
77 else:
78 print('perm_dict中配置有该权限,当前用户没有该权限')
79 return False
80 print('perm_dict中没有配置该权限,当前用户没有该权限')
81 return False
82
83
84 # 装饰器
85 def check_permission(func):
86 def inner(*args,**kwargs):
87 if not perm_check(*args,**kwargs):
88 request = args[0] # 传入的request
89 return render(request, 'kingadmin/page_403.html')
90 return func(*args,**kwargs)
91 return inner
权限配置条目
1 'crm_table_list':['table_obj_list','GET',[],{},view_my_own_customers]
看最后面我们加入的only_view_own_customers就是开发人员自已加的权限控制逻辑,里面想怎么写就怎么写
1 def view_my_own_customers(request):
2 """只能查看自己创建的客户"""
3 print("running permission hook check......")
4
5 if str(request.user.id) == request.GET.get('consultant'): # 查看用GET,判断ID是否与自己ID相同
6 print("只允许访问自己创建的客户")
7 return True
8 print("不允许访问别人创建的客户")
9 return False
这样,万能且通用的权限框架就开发完毕了,权限的控制粒度,可粗可细、可深可浅,包君满意!以后要移植到其它django项目时, 你唯一需要改的,就是配置好perm_dic里的权限条目!
用完觉得好,记得点赞噢!