RABC權限控制(二級菜單實現)

  目前大部分系統因爲用戶體驗,基本上菜單不會作的很深,以二級菜單爲例,作了一個簡單的權限控制實現,可精確到按鈕級別(基於django),下面具體看看實現html

1.表結構的設計node

不管開發什麼都須要先梳理清楚需求,而後再考慮表結構,這裏先來講說大體的表結構組成,注意,個人權限控制是經過url作的,因此控制的核心就在於控制urlpython

表字段介紹設計以下:django

權限表
    url   # 權限
    title  #權限的標題,左側展現,表明的功能(由於不可能展現url吧)
    menu    # 所屬的一級菜單,外鍵關聯一級菜單
    parent    # 二級菜單下的子權限,相似xx列表,旗下的增刪改就是子權限,因此這個須要外鍵自關聯當前表
    url_name    # url分發的別名,主要是用於按鈕級別權限控制,也是爲了以後的擴展
    icon    # 二級菜單的圖標

角色表
    name    # 角色的名稱
    permissions    # 與權限表多對多的關係,一個角色能夠有多個權限,一個權限也能夠給多個角色

用戶表
    name    # 用戶名
    pwd      # 加密後的密碼
    roles     # 與角色表是多對多的關係

一級菜單表
    title    # 一級菜單的標題
    icon    # 一級菜單的圖標
    weight    # 一級菜單的權重,經過權重控制一級菜單的順序,權重最大在最上面

總體邏輯就是創了用戶後,能夠給該用戶分配角色,因爲角色擁有特定權限,因此用戶久擁有了相應的權限

代碼以下:session

from django.db import models


class Menu(models.Model):
    """
    一級菜單表
    """
    title = models.CharField(max_length=32, verbose_name='一級菜單')
    icon = models.CharField(max_length=32, verbose_name='圖標', null=True, blank=True)
    weight = models.IntegerField(verbose_name='菜單權重', default=1)

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    權限表
    """
    title = models.CharField(max_length=32, verbose_name='標題')
    url = models.CharField(max_length=32, verbose_name='權限')
    icon = models.CharField(max_length=32, verbose_name='圖標', null=True, blank=True)
    menu = models.ForeignKey(to='Menu', verbose_name='所屬菜單', on_delete=models.CASCADE, null=True, blank=True)
    url_name = models.CharField(max_length=32, verbose_name='url別名', null=True, blank=True)
    parent = models.ForeignKey('self', on_delete=models.CASCADE, verbose_name='父級菜單', null=True, blank=True)
    # is_menu = models.BooleanField(default=False, verbose_name='是不是菜單')

    class Meta:
        verbose_name_plural = "權限表"
        verbose_name = '權限表'

    def __str__(self):
        return self.title


class Role(models.Model):
    name = models.CharField(max_length=32, verbose_name='角色名稱')
    permissions = models.ManyToManyField(to='Permission', verbose_name='角色所擁有的權限', blank=True)

    def __str__(self):
        return self.name


class User(models.Model):
    """
    用戶表
    """
    name = models.CharField(max_length=32, verbose_name='用戶名')
    pwd = models.CharField(max_length=32, verbose_name='密碼')
    roles = models.ManyToManyField(to='Role', verbose_name='用戶所擁有的角色', blank=True)

    def __str__(self):
        return self.name

四個模型,6張表,由於用戶和角色,角色和權限都是多對多的關係,因此django會自動生成兩張表記錄多對多的關係.數據結構

 

2.成功登陸後初始化用戶信息(合適的數據結構的設計很是重要也比較難)app

將表數據錄入後,就表示用戶擁有了不一樣權限,所以咱們以用戶身份去登陸平臺,首先登錄平臺須要登陸,所以在中間件中須要先設置白名單,避免登陸,註冊等url也被權限控制攔截,登陸認證成功後,就將相關信息存入用戶的session中,這裏來詳細說明下存了哪些信息,直接看代碼,我在代碼裏面進行步驟和備註說明ide

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: Xiaobai Lei
from rbac.models import Role


def initial_session(user_obj, request):
    """
    將當前登陸人的信息記錄到session中
    :param user_obj: 用戶對象
    :param request:
    :return:
    """
    # 1.查詢當前登陸人的權限列表,取出相關的信息
    permissions = Role.objects.filter(user=user_obj).values('permissions__url',
                                                            'permissions__pk',
                                                            'permissions__title',
                                                            'permissions__icon',
                                                            'permissions__parent_id',
                                                            'permissions__url_name',
                                                            'permissions__menu__pk',
                                                            'permissions__menu__title',
                                                            'permissions__menu__weight',
                                                            'permissions__menu__icon').distinct()
    # 2.保存登陸人的相關信息(權限列表,別名列表和權限菜單字典)
    """
    權限列表,以字典形式存儲當前用戶的每個權限信息,數據格式以下:
    permission_list = [
        {'id': 1, 'url': '/custmer/list/', 'title': '客戶列表', 'parent_id': None},
        {'id': 2, 'url': '/custmer/add/', 'title': '客戶添加', 'parent_id': 1},
    ]
    原先簡單設計只是列表保存當前用戶的全部url,後面發現訪問子權限(好比客戶添加)時,依舊須要左側客戶列表展現,
    因此須要用到父權限(客戶列表)的信息,並且爲了更多擴展,因此採用了列表嵌套字典的形式保存了較多數據
    """
    # 權限列表,主要用於用戶的權限校驗
    permission_list = []
    # 別名列表,主要用於按鈕級別的控制,好比客戶添加的按鈕
    permission_url_names = []
    """
    權限菜單字典,數據格式以下:
    permission_menu_dict = {
        '一級菜單id': {
            'menu_title': '信息管理',
            'menu_icon': '一級菜單圖標',
            'menu_weight': '一級菜單的權重',
            'menu_children': [
                    {'id': 1, 'url': '/custmer/list/', 'title': '客戶列表', 'parent_id': None},
                ]
        }
    }
    注意:menu_chidren只保存的是二級菜單(如客戶列表),經過這個數據結構就能夠很清晰的看到層級關係了,若是還有一級菜單
    的話,那麼就須要在客戶列表字典結構中再加入一個node_children:[{}],就是一個不斷循環嵌套的過程,你懂的
    """
    # 權限菜單字典,主要用於左側菜單的數據展現
    permission_menu_dict = {}

    # 循環獲取上面說起的數據結構
    for item in permissions:
        permission_list.append({
            'url': item['permissions__url'],
            'id': item['permissions__pk'],
            'parent_id': item['permissions__parent_id'],
            'title': item['permissions__title'],
        })
        permission_url_names.append(item['permissions__url_name'])
        menu_id = item['permissions__menu__pk']
        # 只有二級菜單才被加入,也就是父權限(如客戶列表)
        if menu_id:
            # 若是字典中已經存在了菜單id就直接在一級菜單的menu_chidren下追加,沒有則先新建
            if menu_id not in permission_menu_dict:
                permission_menu_dict[menu_id] = {
                    'menu_title': item['permissions__menu__title'],
                    'menu_icon': item['permissions__menu__icon'],
                    'menu_weight': item['permissions__menu__weight'],
                    'menu_children': [
                        {
                          'title':  item['permissions__title'],
                          'url':  item['permissions__url'],
                          'icon':  item['permissions__icon'],
                          'id':  item['permissions__pk'],
                        },
                    ]
                }
            else:
                permission_menu_dict[menu_id]['menu_children'].append({
                    'title': item['permissions__title'],
                    'url': item['permissions__url'],
                    'icon': item['permissions__icon'],
                    'id':  item['permissions__pk'],
                })

    # 根據一級菜單權重進行從新排序
    permission_menu_dict_new = {}
    for i in sorted(permission_menu_dict, key=lambda x: permission_menu_dict[x]['menu_weight'], reverse=True):
        permission_menu_dict_new[i] = permission_menu_dict[i]

    # 將用戶的權限列表和權限菜單列表注入session中
    request.session['permission_list'] = permission_list
    request.session['permission_url_names'] = permission_url_names
    request.session['permission_menu_dict'] = permission_menu_dict_new

 

3.權限校驗(採用django自定義中間件)函數

因爲每次訪問都是須要進行權限校驗的,所以就放在了中間件中,以前也提到過,在權限校驗以前你必須是登陸成功的用戶,所以中間件中還加入了用戶認證,具體請見以下:加密

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: Xiaobai Lei
import re

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import (
    redirect, reverse, HttpResponse
)

from rbac.models import Permission
# 白名單列表
WHITE_URL_LIST = [
    r'^/login/$',
    r'^/logout/$',
    r'^/reg/$',
    r'^/favicon.ico$',
    r'^/admin/.*',
]


class PermissionMiddleware(MiddlewareMixin):
    """權限驗證中間件"""

    def process_request(self, request):
        # 1.當前訪問的url
        current_path = request.path_info

        # 2.白名單判斷,若是在白名單的就直接放過去
        for path in WHITE_URL_LIST:
            if re.search(path, current_path):
                return None

        # 3.檢驗當前用戶是否登陸
        user_id = request.session.get('user_id')
        if not user_id:
            return redirect(reverse('login'))

        # 麪包屑導航欄層級記錄,默認首頁爲第一位,主要存儲title(展現在頁面用)和url(用戶點擊後可直接跳轉到相應頁面)
        request.breadcrumb_list = [
            {
                'title': '首頁',
                'url': '/index/',
            }
        ]
        # 4.獲取用戶權限信息並進行校驗
        permission_list = request.session.get('permission_list')
        for item in permission_list:
            # 因爲url的是以正則形式存儲,所以採用正則與當前訪問的url進行徹底匹配,若是符合則證實有權限
            if re.search('^{}$'.format(item['url']), current_path):
                # 將當前訪問路徑的所屬菜單pk記錄到show_id中,用戶訪問子權限時依舊會顯示父權限(二級菜單)
                request.show_id = item['parent_id'] or item['id']
                # 將當前訪問的父子信息記錄到breadcrumb_list中(麪包屑導航欄)
                # 若是是子權限的話,就根據父權限id查出父權限信息,將父權限和子權限都記錄下來
                parent_obj = Permission.objects.filter(pk=item['parent_id']).first()
                if item['parent_id']:
                    request.breadcrumb_list.extend([
                        {
                            'title': parent_obj.title,
                            'url': parent_obj.url,
                        },
                        {
                            'title': item['title'],
                            'url': item['url'],
                        }])
                else:
                    # 排除首頁,由於首頁初始化就存在了
                    if item['title'] != '首頁':
                        request.breadcrumb_list.append({
                            'title': item['title'],
                            'url': item['url'],
                        })
                return None
        else:
            return HttpResponse("無此權限")

 

4.自定義通用模板(inclusion_tag)

經過上面的校驗後,若是該用戶有權限則進入系統,而且展現左側菜單,但在此時想一下,若是是直接展現的話那麼就意味着每個視圖函數(django業務邏輯處理相關)都須要返回菜單的數據給模板層,所以在這裏就用到了inclusion_tag通用模板,注意:須要新建一個包,名稱必須是templatetags,在包下我新建了一個my_tag.py文件,存放一下內容

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: Xiaobai Lei
from django.template import Library


register = Library()


# 獲取左側菜單數據給menu.html而後進行展現
@register.inclusion_tag('rbac/menu.html')
def get_menu_displays(request):
    # 獲取菜單的字典數據
    permission_menu_dict = request.session.get('permission_menu_dict')
    # 循環獲取每一個菜單信息
    for menu in permission_menu_dict.values():
        # 默認二級菜單都是隱藏狀態
        menu['class'] = 'hide'
        # 循環獲取每一個二級菜單信息
        for reg in menu['menu_children']:
            # if re.search("^{}$".format(reg['url']), request.path):
            # 在中間件處理時就已經將父子權限的show_id都變成了父權限的id,以此來表示不管操做哪個,左側父權限菜單都是被選中狀態
            if request.show_id == reg['id']:
                reg['class'] = 'active'
                # 顯示二級菜單
                menu['class'] = ''
    return {'permission_menu_dict': permission_menu_dict}


@register.filter
def url_is_permission(url, request):
    """判斷當前按鈕url是否在權限列表"""
    permission_url_names = request.session.get('permission_url_names')
    return url in permission_url_names

 

5.menu.html(循環展現菜單)

<div class="multi-menu">
    {% for item in permission_menu_dict.values %}
        <div class="item">
            <div class="title"><i style="margin-right: 3px" class="fa {{ item.menu_icon }}"></i>{{ item.menu_title }}</div>
            <div class="body  {{ item.class }}">
                {% for menu_chidren in item.menu_chidren %}
                    <a class="{{ menu_chidren.class }}" href="{{ menu_chidren.url }}">
                    <span class="icon-wrap"><i style="margin-right: 3px" class="fa {{ menu_chidren.icon }}"></i></span>{{ menu_chidren.title }}</a>
                {% endfor %}
            </div>
        </div>
    {% endfor %}
</div>

 

6.最後須要在業務的html中應用自定義的inclusion_tag

 {% load my_tag %}
 {% get_menu_displays request %}

 

7.按鈕級別控制(針對不一樣二級菜單頁面進行按鈕控制),以下

{% if 'customer_edit'|url_is_permission:request %}
     <a style="color: #333333;" href="/customer/edit/{{ row.id }}/">
        <i class="fa fa-edit" aria-hidden="true"></i></a>
{% endif %}

 

至此,權限大致開發完成,目前數據還須要本身去admin管理後臺錄入,下一篇我會繼續說一下開發權限管理的功能,這樣就能直接在系統上進行用戶,角色和權限的自由分配了,到時會將權限和CRM項目合爲一體分享源碼.

相關文章
相關標籤/搜索