Django- 開發通用且萬能的的權限框架組件

本節內容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是權限名, 一會咱們須要用過這些權限名來跟用戶進行關聯

  • 後面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裏的權限條目!

相關文章
相關標籤/搜索