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

本節內容

需求討論

權限設計

代碼設計

自定義權限鉤子 

 

業務場景分析


假設我們在開發一個培訓機構的 客戶關係管理系統,系統分客戶管理、學員管理、教學管理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.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]

        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 

                #逐個匹配參數,看每個參數時候都能對應的上。
                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

                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,'kingadmin/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裏的權限條目!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章