Django之RBAC應用組件


Django之rbac應用組件

一、權限管理的訪問控制

  權限管理,一般指控制用戶的訪問權限,使得用戶可以訪問而且只能訪問自己被授權的資源,不能多也不能少。現在的軟件系統里基本上都用到了權限管理,只是控制的粒度、層面和側重點會有所不同,比較完善的權限管理包括四個方面的訪問控制:

      1.功能(最基礎):以用戶完成某一功能為准。如“添加用戶”、“刪除用戶”

      2.數據:比功能訪問權限的控制粒度更小,如“管理員可看到比一般用戶更多的信息”

      3.時間:給訪問權限添加時間控制,讓訪問的資源在某一時間段中可用。如”12306只能在7:00-23:00時間段內購票“

      4.空間:給訪問權限添加空間控制,根據訪問用戶的空間位置不同,而對用戶的訪問資源進行限制。如”很多人都在問,為什么在中國上不了facebook……“

二、設計理念

   權限管理的設計理念有很多,像ABAC(基於屬性的訪問控制)、ACL(基於資源的訪問控制)、RBAC(基於資源的訪問控制)、GBAC(基於組的訪問控制)等等,它們各有利弊,現在最常用的是RBAC,理論較完善。

  1.基於資源的權限控制——ACL

  ACL(Access Control List)訪問控制列表,是最早也是最基本的一種訪問控制機制,它的權限控制是圍繞”資源“展開的,即每一項資源,都配有一個列表,這個列表記錄的是哪些用戶可以對這些資源進行哪些操作

    這種訪問控制非常簡單,只要把用戶和資源連接起來就行了,但是當用戶和資源增多時,就會產生大量的訪問權限列表,管理這些訪問控制列表本身就是一件非常繁重的工作,這樣便使得ACL在性能上無法勝任實際應用,所以說性能是硬傷 

  2.基於角色的權限控制——RBAC 

  RBAC(Role-Based Access Control)基於角色的訪問控制,在這種訪問控制機制中,引入了Role的概念,將用戶與權限之間的關系進行了解耦,讓用戶通過角色與權限進行關聯。在RBAC模型中,who、what、how構成了訪問權限三元組,即”who對what進行how的操作“。

      一個用戶可有多種角色,每一種角色擁有多個權限,在用戶與角色、角色與權限之間,都是多對多的關系。通過給用戶分配角色,使得用戶擁有對系統的部分使用權限。在實際設計的過程中,可讓角色和資源直接進行綁定,權限控制體現在角色與資源的關聯上

 

      角色,是一定數量的權限集合,也可看成是對擁有相同角色的用戶進行的分類。

 

  3.引入”組“概念的權限控制——RBAC 

      這種方案是比較簡單的權限管理,一般情況下這樣的設計已經足夠了,但是如果要給一組用戶直接分配權限的話就有問題了,所以又引入了用戶組的概念。

 

用戶組,是一組用戶的集合,一個用戶組擁有多個權限。

      通過用戶組的啟發,其實我們也可以增加角色組、資源組等等各種組,實現相應的繼承功能。不過這樣就有點繁瑣了,還是用的時候根據實際需求權衡吧。 

三、基於RBAC的權限管理

      在上邊的分析中,得出了一種包含用戶、角色、權限、組等幾個主體的權限管理,它們之間的關聯都是多對多的,由此得到的ER圖如下:

 RBAC權限管理模型在加入”組“概念后,在實現繼承功能的基礎上,更加靈活的適應了需求的變更。它主要的配置為:用戶-角色配置、用戶-用戶組配置,角色-權限配置,用戶組-資源配置,這些配置對應到數據庫中就是兩個主表之間的第三張表,里邊存儲的是用戶操作的記錄,服務於主表以供查詢。

四、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判斷是是否顯示相關的權限。

  整個流程如下圖:

五、Python實現代碼:

代碼目錄:

  

import re
from django.conf import settings
from django.shortcuts import HttpResponse,render
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 RbacMiddleware(MiddlewareMixin):
    def process_request(self,request):
        # 當前訪問的URL
        current_url = request.path_info
        # print("type(current_url):",type(current_url))
        # print("type(current_url):",type(current_url.split("/")[3]))
        # if "/favicon.ico"==request.path_info :
        #     return HttpResponse("404")
        for valid in settings.VALID_LIST:
            if re.match(valid,current_url):
                return None

        # 當前用戶的所有權限
        permission_dict = request.session.get(settings.PERMISSION_DICT_SESSION_KEY)
        print("permission_dict:",permission_dict)
        if not permission_dict:
            # return HttpResponse('當前用戶無權限信息')
            return HttpResponse('當前用戶未登錄!')

        # 用戶權限和當前URL進行匹配
        flag = False
        for item in permission_dict.values():

            urls = item['urls']
            codes = item['codes']
            for rex in urls:
                reg = settings.REX_FORMAT %(rex,)
                print(rex,current_url)
                if re.match(reg,current_url):
                    flag = True
                    request.permission_codes = codes
                    break
            if flag:
                break

        if not flag:
            # return HttpResponse('無權限訪問,請聯系管理員。')
            return render(request,"404.html")
rbac.py
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-01-09 06:36
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Department',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=32)),
            ],
        ),
        migrations.CreateModel(
            name='Host',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('hostname', models.CharField(max_length=32, verbose_name='主機名')),
                ('ip', models.CharField(max_length=32, verbose_name='IP')),
                ('port', models.IntegerField(verbose_name='端口')),
                ('dp', models.ManyToManyField(to='rbac.Department', verbose_name='部門')),
            ],
        ),
        migrations.CreateModel(
            name='Menu',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=32)),
            ],
        ),
        migrations.CreateModel(
            name='Permission',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=32, verbose_name='權限名稱')),
                ('url', models.CharField(max_length=255, verbose_name='含正則的URL')),
                ('code', models.CharField(max_length=32, verbose_name='權限代碼')),
            ],
        ),
        migrations.CreateModel(
            name='PermissionGroup',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('caption', models.CharField(max_length=32)),
                ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rbac.Menu', verbose_name='所屬菜單')),
            ],
        ),
        migrations.CreateModel(
            name='Role',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=32, verbose_name='角色名稱')),
                ('permissions', models.ManyToManyField(to='rbac.Permission', verbose_name='擁有權限')),
            ],
        ),
        migrations.CreateModel(
            name='UserInfo',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('username', models.CharField(max_length=32, verbose_name='用戶名')),
                ('password', models.CharField(max_length=64, verbose_name='密碼')),
                ('roles', models.ManyToManyField(to='rbac.Role', verbose_name='擁有角色')),
            ],
        ),
        migrations.AddField(
            model_name='permission',
            name='group',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rbac.PermissionGroup', verbose_name='所屬權限組'),
        ),
        migrations.AddField(
            model_name='permission',
            name='group_menu',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='xxx', to='rbac.Permission', verbose_name='組內菜單'),
        ),
        migrations.AddField(
            model_name='host',
            name='user',
            field=models.ManyToManyField(default=1, to='rbac.UserInfo', verbose_name='用戶名'),
        ),
    ]
0001_initial.py
from django.conf import settings

def init_permission(user,request):
    """
    用於做用戶登錄成功之后,權限信息的初始化。
    :param user: 登錄的用戶對象
    :param request: 請求相關的對象
    :return:
    """


    """
                [
                    {'permissions__title': '用戶列表', 'permissions__url': '/users/', 'permissions__code': 'list', 'permissions__group_id': 1}
                    {'permissions__title': '添加用戶', 'permissions__url': '/users/add/', 'permissions__code': 'add', 'permissions__group_id': 1}
                    {'permissions__title': '刪除用戶', 'permissions__url': '/users/del/(\\d+)/', 'permissions__code': 'del', 'permissions__group_id': 1}
                    {'permissions__title': '修改用戶', 'permissions__url': '/users/edit/(\\d+)/', 'permissions__code': 'edit', 'permissions__group_id': 1}
                    {'permissions__title': '主機列表', 'permissions__url': '/hosts/', 'permissions__code': 'list', 'permissions__group_id': 2}
                    {'permissions__title': '添加主機', 'permissions__url': '/hosts/add/', 'permissions__code': 'add', 'permissions__group_id': 2}
                    {'permissions__title': '刪除主機', 'permissions__url': '/hosts/del/(\\d+)/', 'permissions__code': 'del', 'permissions__group_id': 2}
                    {'permissions__title': '修改主機', 'permissions__url': '/hosts/edit/(\\d+)/', 'permissions__code': 'edit', 'permissions__group_id': 2}
                ]

                {
                    1(權限組ID): {
                        urls: [/u sers/,/users/add/ ,/users/del/(\d+)/],
                        codes: [list,add,del]
                    },
                    2: {
                        urls: [/hosts/,/hosts/add/ ,/hosts/del/(\d+)/],
                        codes: [list,add,del]
                    }
                }

                """
    permission_list = user.roles.filter(permissions__id__isnull=False).values(
        'permissions__id',    # 權限ID
        'permissions__title', # 權限名稱
        'permissions__url',   # 權限URL
        'permissions__code',  # 權限CODE
        'permissions__group_menu_id',  # 組內菜單ID(null表示自己是菜單,1)
        'permissions__group_id', # 權限組ID
        'permissions__group__menu__id', # 一級菜單ID
        'permissions__group__menu__name', # 一級菜單名稱
    ).distinct()

    # 獲取權限信息+組+菜單,放入session,用於以后在頁面上自動生成動態菜單。
    permission_memu_list = []
    for item in permission_list:
        val = {
            'id':item['permissions__id'],
            'title':item['permissions__title'],
            'url':item['permissions__url'],
            'pid':item['permissions__group_menu_id'],
            'menu_id':item['permissions__group__menu__id'],
            'menu__name':item['permissions__group__menu__name'],
        }
        permission_memu_list.append(val)
    request.session[settings.PERMISSION_MENU_SESSION_KEY] = permission_memu_list



    # 獲取權限信息,放入session,用於以后在中間件中權限進行匹配
    permission_dict = {}
    for permission in permission_list:
        group_id = permission['permissions__group_id']
        url = permission['permissions__url']
        code = permission['permissions__code']
        if group_id in permission_dict:
            permission_dict[group_id]['urls'].append(url)
            permission_dict[group_id]['codes'].append(code)
        else:
            permission_dict[group_id] = {'urls': [url, ], 'codes': [code, ]}
    request.session[settings.PERMISSION_DICT_SESSION_KEY] = permission_dict
init_permission.py
.menu-item .menu-title{
    height: 30px;
    background-color: cornflowerblue;
}
.menu-item .menu-content{
    margin-left: 20px;
}
.menu-item .menu-content a{
    display: block;
}
.menu-item .menu-content a.active{
    color: red;
}
.hide{
    display: none;
}
rbac.css
{% for menu in menu_result.values %}
    <div class="menu-item">
        <div class="menu-title">{{ menu.menu__name }}</div>
{#                    {% if menu.active %}#}
{#                        <div class="menu-content">#}
{#                    {% else %}#}
{#                        <div class="menu-content hide">#}
{#                    {% endif %}#}
        <div class="menu-content">
            {% for per in menu.children %}
                {% if per.active %}
                    <a href="{{ per.url }}" class="active">{{ per.title }}</a>
                {% else %}
                    <a href="{{ per.url }}">{{ per.title }}</a>
                {% endif %}
            {% endfor %}
        </div>
    </div>
{% endfor %}
menu.html
import re
from django.template import Library
from django.conf import settings
register = Library()


"""
{% menu request %}
"""

@register.inclusion_tag('rbac/menu.html')
def menu(request):
    current_url = request.path_info
    # 獲取session中菜單信息,自動生成二級菜單【默認選中,默認展開】
    permission_menu_list = request.session.get(settings.PERMISSION_MENU_SESSION_KEY)
    per_dict = {}
    for item in permission_menu_list:
        if not item['pid']:
            per_dict[item['id']] = item

    for item in permission_menu_list:
        reg = settings.REX_FORMAT % (item['url'])
        if not re.match(reg, current_url):
            continue
        # 匹配成功
        if item['pid']:
            per_dict[item['pid']]['active'] = True
        else:
            item['active'] = True

    """
    {
        1: {'id': 1, 'title': '用戶列表', 'url': '/users/', 'pid': None, 'menu_id': 1, 'menu__name': '菜單1', 'active': True}, 
        5: {'id': 5, 'title': '主機列表', 'url': '/hosts/', 'pid': None, 'menu_id': 1, 'menu__name': '菜單1'}
        10: {'id': 10, 'title': 'xx列表', 'url': '/hosts/', 'pid': None, 'menu_id': 2, 'menu__name': '菜單2'}
    }

    {
        1:{
            'menu__name': '菜單1',
            'active': True,
            'children':[
                {'id': 1, 'title': '用戶列表', 'url': '/users/','active': True}
                {'id': 5, 'title': '主機列表', 'url': '/users/'}
            ]
        },
        2:{
             'menu__name': '菜單1',
              'children':[
                {'id': 10, 'title': 'xx列表', 'url': '/hosts/'}
            ]

        }
    }
    """

    menu_result = {}
    for item in per_dict.values():
        menu_id = item['menu_id']
        if menu_id in menu_result:
            temp = {'id': item['id'], 'title': item['title'], 'url': item['url'], 'active': item.get('active', False)}
            menu_result[menu_id]['children'].append(temp)
            if item.get('active', False):
                menu_result[menu_id]['active'] = item.get('active', False)
        else:
            menu_result[menu_id] = {
                'menu__name': item['menu__name'],
                'active': item.get('active', False),
                'children': [
                    {'id': item['id'], 'title': item['title'], 'url': item['url'], 'active': item.get('active', False)}
                ]
            }
    return {'menu_result':menu_result}
rbac.py
# -*- coding: utf-8 -*-
__author__ = 'ShengLeQi'
from  django.forms  import Form,ModelForm
from  django.forms import fields
from  django.forms import  widgets

from  rbac import  models
from  django import  forms
from django.core.exceptions import NON_FIELD_ERRORS,ValidationError

class LoginForm(Form):
    username=fields.CharField(
        label="用戶名",
        required=True,
        error_messages={
            'required':'用戶名不能為空',
        },
        widget=widgets.TextInput(attrs={'class':'form-control'})
    )
    password=fields.CharField(
        label='密碼',
        required=True,
        error_messages={
            'required': '密碼不能為空'
        },
        widget=widgets.PasswordInput(attrs={'class':'form-control'})

    )

class HostModelsForm(ModelForm):
    class Meta:
        model=models.Host
        fields=("hostname","ip","port","user","dp")
        widgets = {
            'hostname': widgets.TextInput(attrs={"class": "form-control"}),
            'ip': widgets.TextInput(attrs={"class": "form-control"}),
            'port': widgets.NumberInput(attrs={"class": "form-control"}),
            'user': widgets.SelectMultiple(attrs={"class": "form-control"}),
            'dp': widgets.SelectMultiple(attrs={"class": "form-control"}),
        }

        labels={
            "ip":"IP",
            "port":"端口",
        }
        error_messages={
            "ip":{
                "required":"IP不能為空",
            }
        }


class RegForm(Form):
    username=forms.CharField(label="用戶名", min_length=3,
          widget=widgets.TextInput(attrs={"class": "form-control"})
                             )
    password=forms.CharField(label="密碼", min_length=3,
          widget=widgets.PasswordInput(attrs={"class": "form-control"})
                             )
    re_password=forms.CharField(label="確認密碼", min_length=3,
           widget=widgets.PasswordInput(attrs={"class": "form-control"})
                                    )
    email=forms.EmailField(label="郵箱", min_length=3,
           widget=widgets.TextInput(attrs={"class": "form-control"})
                            )

    def clean_username(self):

        return self.cleaned_data.get("username")

    def clean(self):
        if self.cleaned_data.get("password")==self.cleaned_data.get("re_password"):
            return self.cleaned_data
        else:
            raise ValidationError("兩次密碼不一致")

class UserInfoModelsForm(ModelForm):
    class Meta:
        model=models.UserInfo
        fields=("username","password","roles")
        widgets = {
            'username': widgets.TextInput(attrs={"class": "form-control"}),
            'password': widgets.PasswordInput(attrs={"class": "form-control"}),
            'roles': widgets.SelectMultiple(attrs={"class": "form-control"}),
        }

        labels={
            "username":"用戶名",
            "password":"密碼",
            "roles":"角色",
        }
        error_messages={
            "ip":{
                "required":"IP不能為空",
            }
        }
forms.py
from django.db import models

class UserInfo(models.Model):
    """
    用戶表
        1      alex        123
        2      tianle      123
        2      yanglei      123

    """
    username = models.CharField(verbose_name='用戶名',max_length=32)
    password = models.CharField(verbose_name='密碼',max_length=64)

    roles = models.ManyToManyField(verbose_name='擁有角色',to="Role")

    def __str__(self):
        return  self.username

class Role(models.Model):
    """
    角色表
        1    CEO
        2    CTO
        3    UFO
        4    銷售主管
        5    銷售員
    """
    title = models.CharField(verbose_name='角色名稱',max_length=32)

    permissions = models.ManyToManyField(verbose_name='擁有權限',to="Permission")

    def __str__(self):
        return self.title

class Menu(models.Model):
    """
    菜單表
        菜單1:
            用戶權限組
                用戶列表
            主機權限組
                主機列表
    """
    name = models.CharField(max_length=32)

    def __str__(self):
        return  self.name

class PermissionGroup(models.Model):
    """
    權限組
        1    用戶權限組
                用戶列表
        2    主機權限組
                主機列表
    """
    caption = models.CharField(max_length=32)
    menu = models.ForeignKey(verbose_name='所屬菜單',to='Menu')

class Permission(models.Model):
    """
    權限表
                                                                                組內菜單ID
        1     用戶列表      /users/                 list               1            null
        2     添加用戶      /users/add/             add                1            1
        3     刪除用戶      /users/del/(\d+)/       del                1            1
        4     修改用戶      /users/edit/(\d+)/      edit               1            1

        5     主機列表      /hosts/                 list               2            null
        6     添加主機      /hosts/add/             add                2             5
        7     刪除主機      /hosts/del/(\d+)/       del                2             5
        8     修改主機      /hosts/edit/(\d+)/      edit               2             5

    以后獲取當前用戶權限后,數據結構化處理,並放入session
    {
        1: {
            urls: [/users/,/users/add/ ,/users/del/(\d+)/],
            codes: [list,add,del]
        },
        2: {
            urls: [/hosts/,/hosts/add/ ,/hosts/del/(\d+)/],
            codes: [list,add,del]
        }
    }


    """
    title = models.CharField(verbose_name='權限名稱',max_length=32)
    url = models.CharField(verbose_name='含正則的URL',max_length=255)
    code = models.CharField(verbose_name="權限代碼",max_length=32)
    group = models.ForeignKey(verbose_name='所屬權限組',to="PermissionGroup")
    # is_menu = models.BooleanField(verbose_name='是否是菜單')
    group_menu = models.ForeignKey(verbose_name='組內菜單',to="Permission",null=True,blank=True,related_name='xxx')

class Department(models.Model):
    '''
    部門
    '''
    title = models.CharField(max_length=32)

    def __str__(self):
        return self.title

class Host(models.Model):
    '''
    主機相關信息
    '''
    hostname = models.CharField(verbose_name='主機名', max_length=32)
    ip = models.CharField(verbose_name='IP',max_length=32)# ip = models.GenericIPAddressField(protocol='both')
    port = models.IntegerField(verbose_name="端口")
    user = models.ManyToManyField(verbose_name="用戶名",to='UserInfo',default=1)
    dp = models.ManyToManyField(verbose_name="部門",to="Department")

    def __str__(self):
        return  self.hostname
models.py
rbac組件,目的是創建公共app,用於多所有系統增加權限管理。

1. 將rbac組件添加到project中

2. 將rbac/migrations目錄文件刪除(除__init__.py 以外)

3. 錄入權限: 5個類,7張表

4. 配置文件:
    - 中間件
        MIDDLEWARE = [
            'django.middleware.security.SecurityMiddleware',
            'django.contrib.sessions.middleware.SessionMiddleware',
            ...
            'rbac.middlewares.rbac.RbacMiddleware',
        ]
    - 新增配置文件

        # #################### 權限相關配置 #############################
        PERMISSION_DICT_SESSION_KEY = "user_permission_dict_key"
        PERMISSION_MENU_SESSION_KEY = "user_permission_menu_key"

        REX_FORMAT = "^%s$"

        VALID_LIST = [
            '/login/',
            '^/admin/.*',
        ]
5. 自動生成菜單
    在你自己寫的母版中,引入rbac的inclusion_tag,示例:

    {% load rbac %}      導入rbac文件
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="/static/rbac/rbac.css" />    引入rbac生成的菜單樣式
        {% block css %} {% endblock %}
    </head>
    <body>
        <div class="pg-header">
            頭部菜單
        </div>
        <div class="pg-content">
            <div class="menu">
                {% menu request %}      生成動態菜單
            </div>
            <div class="content">
                {% block content %} {% endblock %}
            </div>
        </div>

        {% block js %} {% endblock %}
    </body>
    </html>
README

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM