Django RBAC權限組件

Django——權限組件(中間件判斷用戶權限–URL初級)

大家在學習,寫項目的時候或多或少的瞭解過一些,關於RBAC的知識點。

簡單介紹 RBAC

RBAC是什麼?

RBAC 是基於角色的訪問控制(Role-Based Access Control )在 RBAC 中,權限與角色相關聯,用戶通過成爲適當角色的成員而得到這些角色的權限。這就極大地簡化了權限的管理。這樣管理都是層級相互依賴的,權限賦予給角色,而把角色又賦予用戶,這樣的權限設計很清楚,管理起來很方便。

RBAC介紹。

RBAC 認爲授權實際上是Who 、What 、How 三元組之間的關係,也就是Who 對What 進行How 的操作,也就是“主體”對“客體”的操作。

  • Who:是權限的擁有者或主體(如:User,Role)。

  • What:是操作或對象(operation,object)。

  • How:具體的權限(Privilege,正向授權與負向授權)。

然後 RBAC 又分爲RBAC0、RBAC1、RBAC2、RBAC3。後期我會介紹。

簡單的瞭解了概念後,我們來看下他基本的使用方法。

問題:

在瞭解了 RBAC後知道了,角色都有對應的權限,我們大膽的設想下。在 一個項目中,每一個 點擊事件資源的請求,都可能是會對用戶的權限來進行判斷。結合了 Restful的思想,資源的請求,還分爲:Get,Post,Put,Delete方法。這些方法的權限也都需要來進行判斷。

下面開始演示

Pyhton中RBAC的設計思路

  1. 數據庫層面(models)

    用戶、角色、權限、權限組、菜單(菜單只是爲了在頁面展示以及菜單作用)

  2. 中間件層(middlewares)

    中間件層是在用戶請求服務器最前面的一層過濾系統,在rbac組件中它的作用是:

    a、讓未登錄的用戶無法訪問相應的URL地址。 (用戶登錄之後 才能擁有特定權限,並且把相關權限格式化成字典格式 存入session)

    b、把當前登錄的用戶權限和當前URL匹配 是否有權限,如果沒有就返回404。

  3. view視圖層(view)

    處理:路由系統分配的請求。

  4. HTML層(前端頁面顯示)

    前端顯示頁面(users.html)頁面的時候,繼承了模板頁面(extends “layout.html” ),頁面“ layout.html ”導入了{% load rbac %}。在rbac組件中templatetags文件下的rbac.py:@register.inclusion_tag。

    在templatetags文件下的rbac.py文件內容中已經把用戶相關權限格式化成menu_result,渲染到了rbac下面的menu.html文件裏面。在menu.html裏面已經根據code判斷是是否顯示相關的權限。

  • 整個流程如下圖:
     
    在這裏插入圖片描述

Rbac組件的基本目錄結構:

在這裏插入圖片描述

按照寫的流程,來講解rbac組件中的各個部分,以及功能,

  • models數據庫表設計(models.py)。

爲了在前端頁面實現 2方面的控制,還需要引入兩個表菜單 menu和分組 group

  1. 在一個頁面,當前用戶的權限,例如是否顯示添加按鈕、編輯、刪除等按鈕。

  2. 左側菜單欄的創建。所以一共是5個類,7張表。

# models.py

from django.db import models

class Menu(models.Model):
    '''頁面中的菜單名'''
    title = models.CharField(max_length=32)

class Group(models.Model):
    '''權限url所屬的組'''
    caption = models.CharField(verbose_name='組名稱',max_length=32)
    menu =models.ForeignKey(verbose_name='組所屬菜單',to='Menu',default=1)  # 組所在的菜單

    class Meta:
        verbose_name_plural = 'Group組表'

    def __str__(self):
        return self.caption

class User(models.Model):
    """
    用戶表
    """
    username = models.CharField(verbose_name='用戶名',max_length=32)
    password = models.CharField(verbose_name='密碼',max_length=64)
    email = models.CharField(verbose_name='郵箱',max_length=32)

    roles = models.ManyToManyField(verbose_name='具有的所有角色',to="Role",blank=True)
    class Meta:
        verbose_name_plural = "用戶表"

    def __str__(self):
        return self.username

class Role(models.Model):
    """
    角色表
    """
    title = models.CharField(max_length=32)
    permissions = models.ManyToManyField(verbose_name='具有的所有權限',to='Permission',blank=True)
    class Meta:
        verbose_name_plural = "角色表"

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    權限表
    """
    title = models.CharField(verbose_name='標題',max_length=32)
    url = models.CharField(verbose_name="含正則URL",max_length=64)
    is_menu = models.BooleanField(verbose_name="是否是菜單")

    code = models.CharField(verbose_name='url代碼',max_length=32,default=0)  # 路徑對應的描述名稱
    group = models.ForeignKey(verbose_name='所屬組',to='Group',null=True,blank=True)    # 所屬組

    class Meta:
        verbose_name_plural = "權限表"

    def __str__(self):
        return self.titlemodel
  • service中的 init_permission.py

功能: 在用戶登錄成功的時候,在session中寫入兩個內容:

  1. 拿到當前用戶的權限url(code信息)。

  2. 拿到當前用戶的可以做菜單的url信息。

詳細代碼如下:

# init_permission.py

def init_permission(user, request):
    '''
    前端頁面調用,把當前登錄用戶的權限放到session中,request參數指前端傳入的當前當前login請求時的request
    :param user: 當前登錄用戶
    :param request: 當前請求
    :return: None
    '''
    # 拿到當前用戶的權限信息
    permission_url_list = user.roles.values('permissions__group_id',
                                            'permissions__code',
                                            'permissions__url',
                                            'permissions__group__menu__id',     # 菜單需要
                                            'permissions__group__menu__title',    # 菜單需要
                                            'permissions__title',   # 菜單需要
                                            'permissions__url',     # 菜單需要
                                            'permissions__is_menu',  # 菜單需要
                                            ).distinct()

    # 頁面顯示權限相關,用到了權限的分組,
    dest_dic = {}       # 代表目標訪問的 URL
    for each in permission_url_list:
        if each['permissions__group_id'] in dest_dic:
            dest_dic[each['permissions__group_id']]['code'].append(each['permissions__code'])
            dest_dic[each['permissions__group_id']]['per_url'].append(each['permissions__url'])
        else:
            # 剛循環,先創建需要的結構,並把第一次的值放進去。
            dest_dic[each['permissions__group_id']] = {'code': [each['permissions__code'], ],
                                                       'per_url': [each['permissions__url'], ]}

    request.session['permission_url_list'] = dest_dic

    # 頁面菜單相關
    # 1.去掉不做菜單的url,拿到的結果是menu_list,列表中的元素是字典
    menu_list = []
    for item_dic in permission_url_list:
        if item_dic['permissions__is_menu']:
            temp = {'menu_id':item_dic['permissions__group__menu__id'],
                    'menu_title':item_dic['permissions__group__menu__title'],
                    'permission__title': item_dic['permissions__title'],
                    'permission_url':item_dic['permissions__url'],
                    'permissions__is_menu':item_dic['permissions__is_menu'],
                    'active':False,   # 用於頁面是否被選中,
                    }
            # temp 其實只是給key重新起名字,之前的名字太長了。。。。
            menu_list.append(temp)
    # 執行完成之後是如下的數據,用來做菜單。

    request.session['permission_menu_list'] = menu_list

中間件md

功能:

  1. 白名單驗證;

  2. 驗證是否已經寫入session,即:是否已經登錄;

  3. 當前訪問的url與當前用戶的權限url進行匹配驗證,並在request中寫入code信息,

詳細代碼如下:

import re
from django.shortcuts import render,redirect,HttpResponse
from django.conf import settings

class MiddlewareMixin(object):
    def __init__(self, get_response=None):
        self.get_response = get_response
        super(MiddlewareMixin, self).__init__()

    def __call__(self, request):
        response = None
        if hasattr(self, 'process_request'):
            response = self.process_request(request)
        if not response:
            response = self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(request, response)
        return response

class M1(MiddlewareMixin):
    '''
    判斷用戶有無此url的權限的中間件
    '''
    def process_request(self,request):
        current_url = request.path_info

        # 1.白名單驗證
        valid_url = settings.VALID_URL
        for each in valid_url:
            if re.match(each, current_url):
                return None

        # 2.驗證是否已經寫入session,即:是否已經登錄
        permission_dic = request.session.get('permission_url_list')
        if not permission_dic:
            return redirect('/login/')

        # 3.與當前訪問的url與權限url進行匹配驗證,並在request中寫入code信息,
        flag = False
        for group_id,code_urls in permission_dic.items():
            for url in code_urls['per_url']:
                regax = '^{0}$'.format(url)
                if re.match(regax,current_url):
                    flag = True
                    request.permission_code_list = code_urls['code']  # 在session中增加code的信息,用於在頁面判斷在當前頁面的權限,
                    break
            if flag:
                break

        if not flag:
            return HttpResponse('無權訪問')


    def process_response(self,request,response):
        return response

切記 中間件要在settings中的中間件加入

左側菜單的生成templatetags目錄下的rbac.py

功能:生成頁面中的左側菜單用inclusion_tag標籤

運用:我們只需要在需要用到的文件中引用就可以生成這個菜單部分的內容。

需要用到的模板文件中:

  • {% load rbac %}

  • {% menu_html request %} 這部分就會變成用inclusion_tag生成的menu_html

詳細代碼如下:

# inclusion_tag生成左側菜單

import re

from django.template import Library

register = Library()

# inclusion_tag的結果是:把menu_html函數的返回值,放到menu_html中做渲染,生成一個渲染之後的大字符串,
# 在前端需要顯示這個字符串的地方,只要調用menu_html就可以,如果有菜單需要傳參數,這裏是request,前端模板本來就有request,
@register.inclusion_tag('menu.html')
def menu_html(request):
    current_url = request.path_info

    # 結構化在頁面顯示的menu數據
    menu_list = request.session.get('permission_menu_list')

    menu_show_dic = {}
    for item in menu_list:
        # 先跟當前url進行匹配,如果當前的url在權限URl中,則需要修改當前的active,用於在前端頁面的顯示。
        url = item['permission_url']
        reg = '^{0}$'.format(url)
        if re.match(reg, current_url):
            print('匹配到了')
            item['active'] = True

        if item['menu_id'] in menu_show_dic:
            menu_show_dic[item['menu_id']]['children'].append(
                {'permission__title': item['permission__title'], 'permission_url': item['permission_url'],
                 'active': item['active']})
            if item['active']:
                menu_show_dic[item['menu_id']]['active'] = True
        else:
            menu_show_dic[item['menu_id']] = {'menu_id': item['menu_id'],
                                              'menu_title': item['menu_title'],
                                              'active': False,
                                              'children': [{'permission__title': item['permission__title'],
                                                            'permission_url': item['permission_url'],
                                                            'active': item['active']}, ]
                                              }
            if item['active']:
                menu_show_dic[item['menu_id']]['active'] = True


    return {'menu_dic':menu_show_dic}

需要的模板文件templates下的menu.html

# menu.html

<div class="menu">
    {% for k,menu in menu_dic.items %}
        {# 一級菜單 #}
        <div class="menu_first">{{ menu.menu_title }}</div>

        {# 二級菜單(就是一級菜單下邊的內容) #}
        {% if menu.active %}
            <ul class="">
        {% else %}
            <ul class="hide">
        {% endif %}

    {% for child in menu.children %}
        {% if child.active %}
            <li class="menu_second active"><a href="{{ child.permission_url }}">{{ child.permission__title }}</a></li>
        {% else %}
            <li class="menu_second"><a href="{{ child.permission_url }}">{{ child.permission__title }}</a></li>
        {% endif %}
    {% endfor %}
    </ul>
    {% endfor %}
</div>

使用inclusion_tag的文件示例:

# 這個是django的模板文件
{% load rbac %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}模板{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'rbac/bootstrap-3.3.7/css/bootstrap.min.css' %}">
    <link rel="stylesheet" href="{% static 'rbac/menu.css' %}">
    {% block css %} {% endblock css %}

</head>
<body>
<div class="container-fluid">
    <div class="row">
        <div class="col-md-2 menu">
            {% block menu %}
                {% menu_html request %}    {# 用inclusion_tag生成的menu_html #}
            {% endblock menu %}
        </div>
        <div class="col-md-9">
            {% block content %}
            content
            {% endblock %}
        </div>
    </div>
</div>

以上配置後就可以實現 RBAC的功能了

參考博客:https://www.cnblogs.com/fengqing89/p/8283470.html
參考博客:https://www.cnblogs.com/sheng-247/articles/8269352.html

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