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

 

posted @ 2018-01-11 17:02  ShengLeQi  阅读(5037)  评论(2编辑  收藏  举报