1. 程式人生 > >百萬年薪python之路 -- RBAC角色許可權設計

百萬年薪python之路 -- RBAC角色許可權設計

RBAC(Role-Based Access Control,基於角色的訪問控制),就是使用者通過角色與許可權進行關聯。簡單地說,一個使用者擁有若干角色,每一個角色擁有若干許可權。這樣,就構造成“使用者-角色-許可權”的授權模型。

在這種模型中,使用者與角色之間,角色與許可權之間,一般都是多對多的關係。

角色是什麼?可以理解為一定數量的許可權的集合,許可權的載體。例如:一個論壇系統,“超級管理員”、“版主”都是角色。版主可管理版內的帖子、可管理版內的使用者等,這些是許可權。要給某個使用者授予這些許可權,不需要直接將許可權授予使用者,可將“版主”這個角色賦予該使用者。

當用戶的數量非常大時,要給系統每個使用者逐一授權(授角色),是件非常煩瑣的事情。這時,就需要給使用者分組,每個使用者組內有多個使用者。除了可給使用者授權外,還可以給使用者組授權。這樣一來,使用者擁有的所有許可權,就是使用者個人擁有的許可權與該使用者所在使用者組擁有的許可權之和。

在應用系統中,許可權表現成什麼?對功能模組的操作,對上傳檔案的刪改,選單的訪問,甚至頁面上某個按鈕、某個圖片的可見性控制,都可屬於許可權的範疇。有些許可權設計,會把功能操作作為一類,而把檔案、選單、頁面元素等作為另一類,這樣構成“使用者-角色-許可權-資源”的授權模型。而在做資料表建模時,可把功能操作和資源統一管理,也就是都直接與許可權表進行關聯,這樣可能更具便捷性和易擴充套件性。

RBAC許可權設計:

一張圖搞定RBAC使用者許可權設計:

許可權設計的大致步驟:

# 1 許可權表設計(model)
# 2 許可權分配(資料分配)
# 3 查詢許可權並注入許可權(封裝的session功能)
# 4 許可權驗證(中介軟體)
# 5 動態生成左側選單

CRM的許可權設計

1. 許可權表設計(model)

# 使用者表
class UserInfo(models.Model):
    username = models.CharField(max_length=32)  # 使用者名稱
    password = models.CharField(max_length=32)  # 密碼    最好在註冊時把密碼加密,在這裡沒有
    roles = models.ManyToManyField('Role')  # 關聯到角色表,在資料庫中會生成第三張表rbac_userinfo_roles表

    def __str__(self):
        return self.username
        
        
# 角色表
class Role(models.Model):
    name = models.CharField(max_length=16)  # 角色名
    permissions = models.ManyToManyField('Permission')  # 關聯到許可權表

    def __str__(self):
        return self.name
        
        
# 許可權
class Permission(models.Model):
    title = models.CharField(max_length=32)     # 許可權名,如 賬單管理
    url = models.CharField(max_length=32)       # 許可權的url
    menus = models.ForeignKey('Menu',null=True,blank=True)
    # 關聯Permission表,用於 非選單許可權 關聯 二級選單許可權
    parent = models.ForeignKey('self',null=True,blank=True)
    # url_alias_name 儲存url的別名
    url_alias_name = models.CharField(max_length=32,null=True,blank=True)
    


    def __str__(self):
        return self.title
    
    
# 一級選單資料表
class Menu(models.Model):
    name = models.CharField(max_length=32)      # 一級選單名
    icon = models.CharField(max_length=32, null=True, blank=True)   # 一級選單所用的font-awesome圖示值
    weight = models.IntegerField(default=100)  # 控制選單排序的,權重值越大,選單展示越靠前

    def __str__(self):
        return self.name
# 一級選單的核心思想
    """
        一級選單
        id  name  icon
        1   業務系統  
        2   教務系統
        
        
        許可權表
        id   title          url              menu_id    
        1    客戶展示       /list/            1             
        2    客戶新增       /add/             None          
        3    跟進記錄展示    /plist/          1           
        4    課程記錄       /course/          2
        5    課程記錄新增    /add/course/     None
    
    """

2. 許可權分配(資料分配)

rbac_permission表

rbac_role表

rbac_role_permissions表

rbac_userinfo表

rbac_userinfo_roles表

rbac_menu表

3. 查詢許可權並注入許可權(封裝的session功能)

from rbac import models


# 許可權注入到session中
def init_permission(request, user_obj):
    # 登入成功之後,將該使用者的所有許可權(url)全部加入到session中
    permission_list = models.Role.objects.filter(
        userinfo__username=user_obj.username
    ).values(
        'permissions__url',         # 需要設定許可權的url
        'permissions__title',       # 許可權的名稱(即二級選單名)
        'permissions__pk',          # 許可權表裡對應的主鍵值(id)
        'permissions__menus__pk',   # 一級選單的主鍵值(id)
        'permissions__menus__name', # 一級選單名
        'permissions__menus__icon', # 一級選單的圖示的font-awesome值
        'permissions__menus__weight',# 一級選單的權重(用來對多個一級選單的排序)
        'permissions__parent_id',   # 許可權的id(即二級選單的id值)
        'permissions__url_alias_name'# 許可權的url的別名

    ).distinct()    # 相同的許可權去重
    # queryset物件不能通過Json進行可序列化,所以轉化成List物件
    # # Object of type 'QuerySet' is not JSON serializable
    # request.session['permission_list'] = list(permission_list)
    permission_dict = {}
    url_alias_name = []         # 存放所有的許可權url別名

    # 篩選選單許可權
    menu_dict = {}
    for i in permission_list:
        permission_dict[i.get('permissions__pk')] = i
        url_alias_name.append(i.get('permissions__url_alias_name'))
        if i.get('permissions__menus__pk'):
            if i.get('permissions__menus__pk') in menu_dict:
                menu_dict[i.get('permissions__menus__pk')]['children'].append(
                    {
                        'title': i.get('permissions__title'),
                        'url': i.get('permissions__url'),
                        'second_menu_id': i.get('permissions__pk'),
                    }
                )
            else:
                menu_dict[i.get('permissions__menus__pk')] = {
                    'name': i.get('permissions__menus__name'),
                    'icon': i.get('permissions__menus__icon'),
                    'weight': i.get('permissions__menus__weight'),
                    'children': [
                        {
                            'title': i.get('permissions__title'),
                            'url': i.get('permissions__url'),
                            'second_menu_id': i.get('permissions__pk'),
                        }
                    ]
                }
    # 將選單許可權注入到session
    request.session['menu_dict'] = menu_dict
    request.session['url_alias_name'] = url_alias_name
    request.session['permission_dict'] = permission_dict
    # menu_dict形成以下的資料結構
    '''
        {
            1: {
                'name': '業務系統',
                'icon': 'fa fa-home fa-fw',
                'weight': 100,
                'children': [{
                    'title': '客戶管理',
                    'url': '/customer/list/',
                    'second_menu_id': None,
                }]
            },
            2: {
                'name': '財務系統',
                'icon': 'fa fa-jpy fa-fw',
                'weight': 200,
                'children': [{
                    'title': '賬單管理',
                    'url': '/payment/list/',
                    'second_menu_id': None,
                }]
            }
    }

    '''
    
    """
    # permission_dict 形成以下的資料結構
    {
    1: {
        'permissions__url': '/customer/list/',
        'permissions__title': '客戶管理',
        'permissions__pk': 1,
        'permissions__menus__pk': 2,
        'permissions__menus__name': '業務系統',
        'permissions__menus__icon': 'fafa-homefa-fw',
        'permissions__menus__weight': 200,
        'permissions__parent_id': None,
        'permissions__url_alias_name': 'customer_list'
    },
    2: {
        'permissions__url': '/customer/add/',
        'permissions__title': '新增客戶',
        'permissions__pk': 2,
        'permissions__menus__pk': None,
        'permissions__menus__name': None,
        'permissions__menus__icon': None,
        'permissions__menus__weight': None,
        'permissions__parent_id': 1,
        'permissions__url_alias_name': 'customer_add'
    },
    3: {
        'permissions__url': '/customer/edit/(?P<cid>\\d+)/',
        'permissions__title': '編輯客戶',
        'permissions__pk': 3,
        'permissions__menus__pk': None,
        'permissions__menus__name': None,
        'permissions__menus__icon': None,
        'permissions__menus__weight': None,
        'permissions__parent_id': 1,
        'permissions__url_alias_name': 'customer_edit'
    },
    4: {
        'permissions__url': '/customer/del/(?P<cid>\\d+)/',
        'permissions__title': '刪除客戶',
        'permissions__pk': 4,
        'permissions__menus__pk': None,
        'permissions__menus__name': None,
        'permissions__menus__icon': None,
        'permissions__menus__weight': None,
        'permissions__parent_id': 1,
        'permissions__url_alias_name': 'customer_del'
    },
    5: {
        'permissions__url': '/payment/list/',
        'permissions__title': '賬單管理',
        'permissions__pk': 5,
        'permissions__menus__pk': 1,
        'permissions__menus__name': '財務系統',
        'permissions__menus__icon': 'fafa-rmbfa-fw',
        'permissions__menus__weight': 100,
        'permissions__parent_id': None,
        'permissions__url_alias_name': 'payment_list'
    },
    6: {
        'permissions__url': '/payment/add/',
        'permissions__title': '新增繳費',
        'permissions__pk': 6,
        'permissions__menus__pk': None,
        'permissions__menus__name': None,
        'permissions__menus__icon': None,
        'permissions__menus__weight': None,
        'permissions__parent_id': 5,
        'permissions__url_alias_name': 'payment_add'
    },
    7: {
        'permissions__url': '/payment/edit/(?P<pid>\\d+)/',
        'permissions__title': '編輯繳費',
        'permissions__pk': 7,
        'permissions__menus__pk': None,
        'permissions__menus__name': None,
        'permissions__menus__icon': None,
        'permissions__menus__weight': None,
        'permissions__parent_id': 5,
        'permissions__url_alias_name': 'payment_edit'
    },
    8: {
        'permissions__url': '/payment/del/(?P<pid>\\d+)/',
        'permissions__title': '刪除繳費',
        'permissions__pk': 8,
        'permissions__menus__pk': None,
        'permissions__menus__name': None,
        'permissions__menus__icon': None,
        'permissions__menus__weight': None,
        'permissions__parent_id': 5,
        'permissions__url_alias_name': 'payment_del'
    },
    9: {
        'permissions__url': '/nashui/',
        'permissions__title': '納稅管理',
        'permissions__pk': 9,
        'permissions__menus__pk': 1,
        'permissions__menus__name': '財務系統',
        'permissions__menus__icon': 'fafa-rmbfa-fw',
        'permissions__menus__weight': 100,
        'permissions__parent_id': None,
        'permissions__url_alias_name': 'nashui'
    }
}
    
    """

4. 許可權驗證(中介軟體)

在rbac應用裡新建一個middlewares資料夾(資料夾名隨意),然後新建一個mymiddleware.py檔案(py檔名字隨意)

記得在settings檔案的MIDDLEWARE里加入中介軟體

import re
from django.utils.deprecation import MiddlewareMixin
from django.urls import reverse
from django.shortcuts import redirect, HttpResponse, render


class Auth(MiddlewareMixin):

    def process_request(self, request):

        # 登入認證白名單
        white_list = [reverse('login'), reverse('logout'), ]

        # 許可權認證白名單
        permission_white_list = [reverse('index'), '/admin/*']

        request.pid = None

        bread_crumb = [
            {'url': reverse('index'), 'title': '首頁'}
        ]

        request.bread_crumb = bread_crumb   # 把生成麵包屑的資料放入到request物件中,在42行處加入注入資料
        # 登入認證
        path = request.path
        if path not in white_list:      
            is_login = request.session.get('is_login')
            if not is_login:
                return redirect('login')

            # 許可權認證
            permission_dict = request.session.get('permission_dict')

            for white_path in permission_white_list:
                if re.match(white_path, path):
                    break
            else:
                for i in permission_dict.values():
                    reg = r"^%s$" % i['permissions__url']
                    if re.match(reg, path):
                        pid = i.get('permissions__parent_id')
                        if pid: # 如果這個不是選單許可權,就執行
                            # 父級二級選單路徑資訊
                            request.bread_crumb.append(
                                {
                                    'url': permission_dict[str(pid)]['permissions__url'],   # 麵包屑的父級二級選單url
                                    'title': permission_dict[str(pid)]['permissions__title']    # 麵包屑的父級二級選單名字
                                }
                            )
                            # 子許可權的路徑資訊  #/payment/add/
                            request.bread_crumb.append(
                                {   # 麵包屑的當前許可權url
                                    'url': i.get('permissions__url'),
                                    # 麵包屑的當前許可權名字
                                    'title': i.get('permissions__title')
                                }
                            )
                            request.pid = pid   # 把二級選單的id注入到request裡
                        else:
                            # 二級選單路徑資訊
                            request.bread_crumb.append(
                                {
                                    'url': i.get('permissions__url'),
                                    'title': i.get('permissions__title')
                                }
                            )
                            request.pid = i.get('permissions__pk')
                        break
                else:
                    return HttpResponse('你許可權不足!!!')

5. 動態生成左側選單

採用自定義標籤來完成

  1. 現在rbac裡新建一個templatetags資料夾(資料夾名字必須叫這個)
  2. 在資料夾裡新建一個mytags.py檔案(py檔名字任意)
  3. 在py檔案裡註冊 "註冊器",如下
from django import template
register = template.Library()
  1. 在函式上新增 @register.inclusion_tag('HTML檔名')

mytags檔案:

import re
from collections import OrderedDict
from django import template

register = template.Library()   # 註冊 註冊器register


@register.inclusion_tag('menu.html')
def menu(request):
    menu_dict = request.session.get('menu_dict')
    menu_order_key = sorted(menu_dict, key=lambda x: menu_dict[x]['weight'], reverse=True)  # 對menu_dict裡字典資料排序,
    menu_order_dict = OrderedDict() # 生成有序字典,python3.6以上字典預設順序為加入字典時的順序,可不用OrderedDict
    for key in menu_order_key:
        menu_order_dict[key] = menu_dict[key]
    # path = request.path
    for k, v in menu_order_dict.items():
        v['class'] = 'hidden'
        for i in v['children']:
            # if re.match(i['url'], path):
            if request.pid == i['second_menu_id']:
                v['class'] = ''
                i['class'] = 'active'
    menu_data = {'menu_data': menu_order_dict}
    return menu_data

生成左側選單的HTML檔案

layout.html的核心部分 :

#### HTML中生成左側選單的兩行程式碼
{% load mytags %}
{% menu request %}


##### 麵包屑的程式碼
<div>
    <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">
{#                <li><a href="#">首頁</a></li>#}
{#                <li class="active">客戶管理</li>#}
        {% for  crumb in request.bread_crumb %}
            {% if forloop.last %}
                <li class="active">{{ crumb.title }}</li>
            {% else %}
                <li><a href="{{ crumb.url }}">{{ crumb.title }}</a></li>
            {% endif %}
        {% endfor %}
    </ol>
</div>