本节内容

需求讨论

权限设计

代码设计

自定义权限钩子

业务场景分析

假设我们在开发一个培训机构的 客户关系管理系统,系统分客户管理、学员管理、教学管理3个大模块,每个模块大体功能如下

客户管理
销售人员可以录入客户信息,对客户进行跟踪,为客户办理报名手续
销售人员可以修改自己录入的客户信息
客户信息不能删除
销售主管可以查看销售报表

学员管理 
学员可以在线报名 
学员可以查看自己的报名合同、学习有效期
学员可以在线提交作业 、查看自己的成绩

教学管理
管理员可以创建新课程、班级
讲师可以创建上课纪录
讲师可以在线点名、批作业

从上面的需求中, 我们至少提取出了5个角色,普通销售、销售主管、学员、讲师、管理员, 他们能做的事情都是不一样的

如何设计一套权限组件来实现对上面各种不同功能进行有效的权限控制呢?我们肯定不能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是权限名, 一会我们需要用过这些权限名来跟用户进行关联

  • 后面values列表里第一个值如'table_index'是django中的url name,在这里必须相对的url name, 而不是绝对url路径,因为考虑到django url正则匹配的问题,搞绝对路径,不好控制。
  • values里第2个值是http请求方法
  • values里第3个[]是要求这个请求中必须带有某些参数,但不限定对数的值是什么
  • values里的第4个{}是要求这个请求中必须带有某些参数,并且限定所带的参数必须等于特定的值

有的同学看了上面的几条权限定义后,提出疑问,说你这个权限的控制好像还是粗粒度的, 比如我想控制用户只能访问 客户 表里的 一条或多条特定的用户怎么办?

哈,这个问题很好,但很容易解决呀,只需要在[] 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)

Customer表

里面的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装饰器里干的事情就是以下几步:

  1. 拿到用户请求的url+请求方法+参数到我们的的perm_dic里去一一匹配
  2. 当匹配到了对应的权限条目后,就拿着这个条目所对应的权限名,和当前的用户, 调用request.user.has_perm(权限名)
  3. 如果request.user.has_perm(权限名)返回为True,就认为该用户有权限 ,直接放行,否则,则返回403页面!
    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里的权限条目!

Django- 开发通用且万能的的权限框架组件的更多相关文章

  1. Django万能权限框架组件

    业务场景分析 假设我们在开发一个培训机构的 客户关系管理系统,系统分客户管理.学员管理.教学管理3个大模块,每个模块大体功能如下 客户管理 销售人员可以录入客户信息,对客户进行跟踪,为客户办理报名手续 ...

  2. C#.NET 大型通用信息化系统集成快速开发平台 4.1 版本 - 数据权限增强、范围权限增强

    并不是不想做B\S的管理工具,只是精力实在不够,由于用户权限管理组件是基础组件.所以C\S的也无妨,不会有几个人在乎Oracle,SQLServer是否不b\s的,注重的是功能性能,请大家不要纠结与是 ...

  3. Django开发——集成的子框架django.contrib

    Django开发——集成的子框架django.contrib 2018年09月11日 19:32:42 Mrkang1314 阅读数:63  https://blog.csdn.net/mashaok ...

  4. django开发个人简易Blog——数据模型

    提到数据模型,一定要说一下MVC,MVC框架是现代web开发中最流行的开发框架,它将数据与业务逻辑分开,减小了应用之间的高度耦合.个人非常喜欢MVC开发框架,除了具有上述特性,它使得web开发变得非常 ...

  5. Android 快速开发系列 打造万能的ListView GridView 适配器

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38902805 ,本文出自[张鸿洋的博客] 1.概述 相信做Android开发的写 ...

  6. ASP.NET通用权限验证组件实现

    沙发(SF)通用权限验证组件 开篇 本篇介绍通用权限验证的实现代码思路,总共分为导入参数.解析XML.根据XML配置进行处理.返回结果. 代码架构图 1.   类介绍 1.SFWebPermissio ...

  7. .net通用权限框架B/S(一)

    一直做软件实施,用过一些二次开发平台,最近看了一些大神写的框架,于是参考写了一个B/S通用权限框架,项目使用MVC4+EF5+EASYUI(.net framework4),开发环境vs2010+sq ...

  8. nginx+uwsgi+django开发环境搭建

    Nginx+uWSGI+Djangoi开发环境搭建 Django简介,环境搭建 uWSGI简介,安装与配置 Nginx安装与配置 Nginx+uWSGI+Django原理解析 1.django简介,环 ...

  9. 15款Django开发常用软件包(转)

    原文:http://www.iteye.com/news/28697 Django是一款高级的Python Web框架,可以帮助开发者快速创建web应用.我们这里整理了15款Django开发中常用的软 ...

随机推荐

  1. sprintf 会自动追加’\0’至字符串末尾

    #define LEN_SERIAL_NUMBER (12) void C_CosemObjMeterInfo::SetSerialNum(U08 u08MeterId, U64 u64SerialN ...

  2. Source insight 提示: it is not currently available for write access

    点击阅读原文 使用sourceinsight编辑linux内核文件后不能保存,并且弹出窗口揭示:Error: "Z:\linux\kernel\kernel-2.6.13\scripts\k ...

  3. 基于docker-compose搭建gitlab

    安装及配置 修改docker-compose文件 vim docker-compose.yml gitlab: image: 'gitlab/gitlab-ce:latest' restart: al ...

  4. 国内透明代理IP

  5. 深入理解跨域SSO单点登录原理与技术

    [本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 一:SSO体系结 ...

  6. 手把手教你基于SqlSugar4编写一个可视化代码生成器(生成实体,以SqlServer为例,文末附源码)

    在开发过程中免不了创建实体类,字段少的表可以手动编写,但是字段多还用手动创建的话不免有些浪费时间,假如一张表有100多个字段,手写有些不现实. 这时我们会借助一些工具,如:动软代码生成器.各种ORM框 ...

  7. 史上最经典的git教程

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://wsyht90.blog.51cto.com/9014030/1832284 文档 ...

  8. Java 将PDF/XPS转为Word/html /SVG/PS/PCL/PNG、PDF和XPS互转(基于Spire.Cloud.SDK for Java)

    Spire.Cloud.SDK for Java提供了接口PdfConvertApi通过convert()方法将PDF文档以及XPS文档转为指定文档格式,如转PDF为Word(支持Docx.Doc). ...

  9. maven在idea中的配置的注意点

    1.基本的配置查看尚硅谷的文档链接在下 链接:https://pan.baidu.com/s/18gwll6gU38qNH2P01To-lQ 提取码:oq40 2.注意点: 需要将新建项目的配置也修改 ...

  10. JDK8--02:为什么要使用lambda

    lambda是一个匿名函数,我们可以把lambda理解为一个可以传递的代码(将代码像数据一样传递),可以写出更简洁更灵活的代码.首先看一下原来的匿名内部类实现方式(以比较器为例) //原来的匿名内部类 ...