本节内容html
需求讨论python
权限设计web
代码设计数据库
自定义权限钩子 django
假设咱们在开发一个培训机构的 客户关系管理系统,系统分客户管理、学员管理、教学管理3个大模块,每一个模块大致功能以下网络
客户管理
销售人员能够录入客户信息,对客户进行跟踪,为客户办理报名手续
销售人员能够修改本身录入的客户信息
客户信息不能删除
销售主管能够查看销售报表架构
学员管理
学员能够在线报名
学员能够查看本身的报名合同、学习有效期
学员能够在线提交做业 、查看本身的成绩app
教学管理
管理员能够建立新课程、班级
讲师能够建立上课纪录
讲师能够在线点名、批做业框架
从上面的需求中, 咱们至少提取出了5个角色,普通销售、销售主管、学员、讲师、管理员, 他们能作的事情都是不同的ide
如何设计一套权限组件来实现对上面各类不一样功能进行有效的权限控制呢?咱们确定不能LOW到为每一个动做都一堆代码来控制权限对吧? 这些表面上看着各类不尽相同的功能,确定是能够提取出一些相同的规律的,仔细分析,其实每一个功能本质上都是一个个的动做,若是能把动做再抽象中具体权限条目,而后把这些权限条目 再跟用户关联,每一个用户进行这个动做,就检查他没有这个权限,不就实现权限的控制了么?因为这个系统是基于WEB的B/S架构,咱们能够把每一个动做的构成 提取成如下的元素
一个动做 = 一条权限 = 一个url + 一种请求方法(get/post/put...) + 若干个请求参数
那咱们接下来须要作的,就是把 一条条的权限条目定义出来,而后跟用户关联上就能够了!
权限 就是对 软件系统 中 各类资源 的 访问和操做的控制!
在软件系统中,数据库、内存、硬盘里数据都是资源,资源就是数据!
资源自己是静态的, 必须经过合适的动做对其进行访问和操做,咱们说要控制权限,其实本质上是要对访问 软件中各类数据资源的动做进行控制
动做又能够分为2种:
资源操做动做:访问和操做各类数据资源,好比访问数据库或文件里的数据
业务逻辑事件动做:访问和操做的目的不是数据源自己,而是借助数据源而产生的一系列业务逻辑,好比批量往远程 主机上上传一个文件,你须要从数据库中访问主机列表,但你真正要操做的是远程的主机,这个远程的主机,严格意义上来并非你的数据资源,而是这个资源表明的实体。
咱们把权限组件的实现分3步,权限条目的定义, 权限条目与用户的关联,权限组件与应用的结合
咱们前面讲过如下概念, 如今须要作的,就是把咱们系统中全部的须要控制的权限 所对应的动做 提取成 一条条 url+请求方法+参数的集合就能够
一个动做 = 一条权限 = 一个url + 一种请求方法(get/post/put...) + 若干个请求参数
如下是提取出来的几条权限
1
2
3
4
5
6
7
8
|
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是权限名, 一会咱们须要用过这些权限名来跟用户进行关联
有的同窗看了上面的几条权限定义后,提出疑问,说你这个权限的控制好像仍是粗粒度的, 好比我想控制用户只能访问 客户 表里的 一条或多条特定的用户怎么办?
哈,这个问题很好,但很容易解决呀,只须要在[] or {}里指定参数就可呀,好比要求http请求参数中必须包括指定的参数,举个例子, 个人客户表以下:
class Customer(models.Model): '''存储全部客户信息''' #客户在咨询时,可能是经过qq,因此这里就把qq号作为惟一标记客户的值,不能重复 qq = models.CharField(max_length=64,unique=True,help_text=u'QQ号必须惟一') qq_name = models.CharField(u'QQ名称',max_length=64,blank=True,null=True) #客户只要没报名,你没理由要求人家必须告诉你真实姓名及其它更多私人信息呀 name = models.CharField(u'姓名',max_length=32,blank=True,null=True) sex_type = (('male',u'男'),('female',u'女')) sex = models.CharField(u"性别",choices=sex_type,default='male',max_length=32) birthday = models.DateField(u'出生日期',max_length=64,blank=True,null=True,help_text="格式yyyy-mm-dd") phone = models.BigIntegerField(u'手机号',blank=True,null=True) email = models.EmailField(u'经常使用邮箱',blank=True,null=True) id_num = models.CharField(u'身份证号',blank=True,null=True,max_length=64) source_type = (('qq',u"qq群"), ('referral',u"内部转介绍"), ('website',u"官方网站"), ('baidu_ads',u"百度广告"), ('qq_class',u"腾讯课堂"), ('school_propaganda',u"高校宣讲"), ('51cto',u"51cto"), ('others',u"其它"), ) #这个客户来源渠道是为了之后统计各渠道的客户量\成单量,先分类出来 source = models.CharField(u'客户来源',max_length=64, choices=source_type,default='qq') #咱们的不少新客户都是老学员转介绍来了,若是是转介绍的,就在这里纪录是谁介绍的他,前提这个介绍人必须是咱们的老学员噢,要否则系统里找不到 referral_from = models.ForeignKey('self',verbose_name=u"转介绍自学员",help_text=u"若此客户是转介绍自内部学员,请在此处选择内部\学员姓名",blank=True,null=True,related_name="internal_referral") #已开设的课程单独搞了张表,客户想咨询哪一个课程,直接在这里关联就能够 course = models.ForeignKey("Course",verbose_name=u"咨询课程") class_type_choices = (('online', u'网络班'), ('offline_weekend', u'面授班(周末)',), ('offline_fulltime', u'面授班(脱产)',), ) class_type = models.CharField(u"班级类型",max_length=64,choices=class_type_choices) customer_note = models.TextField(u"客户咨询内容详情",help_text=u"客户咨询的大概状况,客户我的信息备注等...") work_status_choices = (('employed','在职'),('unemployed','无业')) work_status = models.CharField(u"职业状态",choices=work_status_choices,max_length=32,default='employed') company = models.CharField(u"目前就任公司",max_length=64,blank=True,null=True) salary = models.CharField(u"当前薪资",max_length=64,blank=True,null=True) status_choices = (('signed',u"已报名"),('unregistered',u"未报名")) status = models.CharField(u"状态",choices=status_choices,max_length=64,default=u"unregistered",help_text=u"选择客户此时的状态") #课程顾问很得要噢,每一个招生老师录入本身的客户 consultant = models.ForeignKey("UserProfile",verbose_name=u"课程顾问") date = models.DateField(u"咨询日期",auto_now_add=True) def __str__(self): 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
2
3
4
5
6
7
8
|
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字段看到你刚添加的条目。
只要把刚添加 的几条权限 移动的右边的框里,那这个用户就至关于有相应的权限 了!之后,你在代码里经过如下语句,就能够断定用户是否有相应的权限。
1
|
user.has_perm(
'app.view_task'
)
|
看到这,有的同窗还在蒙逼,这个自带的权限跟咱们刚才本身定义的权限条目有半毛钱关系么?聪明的同窗已经看出来了, 只要咱们把刚才本身定义的perm_dic字典里的全部key在这个META类的permissions元组里。就至关于把用户和它能够操做的权限关联起来了!这就省掉了咱们必须本身写权限与用户关联所须要的代码了
咱们但愿咱们的权限组件是通用的,可插拔的,它必定要与具体的业务代码分离,之后能够轻松把这个组件移植到其它的项目里去,所以这里咱们采用装饰器的模式,把权限的检查、控制封装在一个装饰器函数里,想对哪一个Views进行权限控制,就只须要在这个views上加上装饰器就能够了。
1
2
3
|
@check_permission
def
table_change(request,app_name,table_name,obj_id):
.....
|
那这个@check_permission装饰器里干的事情就是如下几步:
from django.urls import resolve from django.shortcuts import render, redirect, HttpResponse from CrmAdmin.permissions_list import perm_dic from django.conf import settings def perm_check(*args, **kwargs): request = args[0] resolve_url_obj = resolve(request.path) current_url_name = resolve_url_obj.url_name # 当前url的url_name print('---perm:', request.user, request.user.is_authenticated, current_url_name) # match_flag = False match_key = None if request.user.is_authenticated is False: return redirect(settings.LOGIN_URL) for permission_key, permission_val in perm_dic.items(): match_results = None per_url_name = permission_val[0] per_method = permission_val[1] perm_args = permission_val[2] perm_kwargs = permission_val[3] if per_url_name == current_url_name: # matches current request url if per_method == request.method: # matches request method if perm_args: # if no args defined in perm dic, then set this request to passed perm # 逐个匹配参数,看每一个参数时候都能对应的上。 args_matched = False # for args only for item in perm_args: request_method_func = getattr(request, per_method) if request_method_func.get(item, None): # request字典中有此参数 args_matched = True else: print("arg not match......") args_matched = False break # 有一个参数不能匹配成功,则断定为假,退出该循环。 else: args_matched = True if perm_kwargs: # 匹配有特定值的参数 kwargs_matched = False for k, v in perm_kwargs.items(): request_method_func = getattr(request, per_method) arg_val = request_method_func.get(k, None) # request字典中有此参数 print("perm kwargs check:", arg_val, type(arg_val), v, type(v)) if arg_val == str(v): # 匹配上了特定的参数 及对应的 参数值, 好比,须要request 对象里必须有一个叫 user_id=3的参数 kwargs_matched = True else: kwargs_matched = False break # 有一个参数不能匹配成功,则断定为假,退出该循环。 else: kwargs_matched = True match_results = [args_matched, kwargs_matched] print("--->match_results ", match_results) if all(match_results): # 都匹配上了 match_key = permission_key break if all(match_results): app_name, *per_name = match_key.split('_') print("--->matched ", match_results, match_key) print(app_name, *per_name) perm_obj = '%s.%s' % (app_name, match_key) print("perm str:", perm_obj) if request.user.has_perm(perm_obj): print('当前用户有此权限') return True else: print('当前用户没有该权限') return False else: print("未匹配到权限项,当前用户无权限") def check_permission(func): def inner(*args, **kwargs): if not perm_check(*args, **kwargs): request = args[0] return render(request, 'crmadmin/page_403.html') return func(*args, **kwargs) return inner
仔细按上面的步骤走下来,并玩了一会的同窗,可能会发现一个问题,这个组件对有些权限是控制不到的, 就是涉及到一些业务逻辑的权限,没办法控制 , 好比 我只容许 用户访问本身建立的客户数据,这个你怎么控制?
经过控制 用户的请求参数 是没办法实现的, 由于你获取到的request.user是个动态的值,你必须经过代码来判断 这条数据 是不是由当前请求用户 建立的。 相似的业务逻辑还有不少?你怎么搞?
仔细思考了10分钟,即然这里必须涉及到必须容许开发人员经过自定义一些业务逻辑代码来判断用户是否有权限的话,那我在个人权限组件里再提供一个权限自定义函数不就能够了,开发者能够把自定的权限逻辑写到函数里,个人权限组件 自动调用这个函数,只要返回为True就认为有权限,就能够啦!
from django.core.urlresolvers import resolve from django.shortcuts import render,redirect,HttpResponse from kingadmin.permission_list import perm_dic from django.conf import settings def perm_check(*args,**kwargs): request = args[0] resolve_url_obj = resolve(request.path) current_url_name = resolve_url_obj.url_name # 当前url的url_name print('---perm:',request.user,request.user.is_authenticated(),current_url_name) #match_flag = False match_key = None if request.user.is_authenticated() is False: return redirect(settings.LOGIN_URL) for permission_key,permission_val in perm_dic.items(): per_url_name = permission_val[0] per_method = permission_val[1] perm_args = permission_val[2] perm_kwargs = permission_val[3] custom_perm_func = None if len(permission_val) == 4 else permission_val[4] if per_url_name == current_url_name: #matches current request url if per_method == request.method: #matches request method # if not perm_args: #if no args defined in perm dic, then set this request to passed perm check # match_flag = True # match_key = permission_key # else: #逐个匹配参数,看每一个参数时候都能对应的上。 args_matched = False #for args only for item in perm_args: request_method_func = getattr(request,per_method) if request_method_func.get(item,None):# request字典中有此参数 args_matched = True else: print("arg not match......") args_matched = False break # 有一个参数不能匹配成功,则断定为假,退出该循环。 else: args_matched = True #匹配有特定值的参数 kwargs_matched = False for k,v in perm_kwargs.items(): request_method_func = getattr(request, per_method) arg_val = request_method_func.get(k, None) # request字典中有此参数 print("perm kwargs check:",arg_val,type(arg_val),v,type(v)) if arg_val == str(v): #匹配上了特定的参数 及对应的 参数值, 好比,须要request 对象里必须有一个叫 user_id=3的参数 kwargs_matched = True else: kwargs_matched = False break # 有一个参数不能匹配成功,则断定为假,退出该循环。 else: kwargs_matched = True #自定义权限钩子 perm_func_matched = False if custom_perm_func: if custom_perm_func(request,args,kwargs): perm_func_matched = True else: perm_func_matched = False #使整条权限失效 else: #没有定义权限钩子,因此默认经过 perm_func_matched = True match_results = [args_matched,kwargs_matched,perm_func_matched] print("--->match_results ", match_results) if all(match_results): #都匹配上了 match_key = permission_key break if all(match_results): app_name, *per_name = match_key.split('_') print("--->matched ",match_results,match_key) print(app_name, *per_name) perm_obj = '%s.%s' % (app_name,match_key) print("perm str:",perm_obj) if request.user.has_perm(perm_obj): print('当前用户有此权限') return True else: print('当前用户没有该权限') return False else: print("未匹配到权限项,当前用户无权限") def check_permission(func): def inner(*args,**kwargs): if not perm_check(*args,**kwargs): request = args[0] return render(request,'kingadmin/page_403.html') return func(*args,**kwargs) return inner
权限配置条目
1
2
3
|
'crm_can_access_my_clients'
:[
'table_list'
,
'GET'
,[],
{
'perm_check'
:
33
,
'arg2'
:
'test'
},
custom_perm_logic.only_view_own_customers],
|
看最后面咱们加入的only_view_own_customers就是开发人员自已加的权限控制逻辑,里面想怎么写就怎么写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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里的权限条目!