RBAC权限控制组件

1. RBAC介绍

RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。

在这种模型中,用户与角色之间,角色与权限之间,一般都是多对多的关系。

角色是什么?可以理解为一定数量的权限的集合,权限的载体。例如:一个论坛系统,“超级管理员”、“版主”都是角色。版主可管理版内的帖子、可管理版内的用户等,这些是权限。要给某个用户授予这些权限,不需要直接将权限授予用户,可将“版主”这个角色赋予该用户。

当用户的数量非常大时,要给系统每个用户逐一授权(授角色),是件非常烦琐的事情。这时,就需要给用户分组,每个用户组内有多个用户。除了可给用户授权外,还可以给用户组授权。这样一来,用户拥有的所有权限,就是用户个人拥有的权限与该用户所在用户组拥有的权限之和。

在应用系统中,权限表现成什么?对功能模块的操作,对上传文件的删改,菜单的访问,甚至页面上某个按钮、某个图片的可见性控制,都可属于权限的范畴。有些权限设计,会把功能操作作为一类,而把文件、菜单、页面元素等作为另一类,这样构成“用户-角色-权限-资源”的授权模型。而在做数据表建模时,可把功能操作和资源统一管理,也就是都直接与权限表进行关联,这样可能更具便捷性和易扩展性。

2. 权限设计的大致步骤:

  1. 权限表设计(model)
  2. 权限分配(数据分配)
  3. 查询权限并注入权限(封装的session功能)
  4. 权限验证(中间件)
  5. 动态生成左侧菜单

3. 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表

1572349367505

rbac_role表

1572349395424

rbac_role_permissions表

1572349420905

rbac_userinfo表

1572349469801

rbac_userinfo_roles表

1572349484328

rbac_menu表

1572349762710

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()
    
  4. 在函数上添加 @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>

4. RBAC角色权限设计

**RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。

# 角色是什么?可以理解为一定数量的权限的集合,权限的载体。例如:一个论坛系统,“超级管理员”、“版主”都是角色。版主可管理版内的帖子、可管理版内的用户等,这些是权限。要给某个用户授予这些权限,不需要直接将权限授予用户,可将“版主”这个角色赋予该用户。
# 当用户的数量非常大时,要给系统每个用户逐一授权(授角色),是件非常烦琐的事情。这时,就需要给用户分组,每个用户组内有多个用户。除了可给用户授权外,还可以给用户组授权。这样一来,用户拥有的所有权限,就是用户个人拥有的权限与该用户所在用户组拥有的权限之和。
# 在应用系统中,权限表现成什么?对功能模块的操作,对上传文件的删改,菜单的访问,甚至页面上某个按钮、某个图片的可见性控制,都可属于权限的范畴。有些权限设计,会把功能操作作为一类,而把文件、菜单、页面元素等作为另一类,这样构成“用户-角色-权限-资源”的授权模型。而在做数据表建模时,可把功能操作和资源统一管理,也就是都直接与权限表进行关联,这样可能更具便捷性和易扩展性。

  • 权限表设计

  • 权限分配(数据分配)

  • 查询权限并注入权限(封装的session功能)

  • 权限验证(中间件)

  • 动态生成左侧菜单

4.1 表关系

4.2 rbac之model:

from django.db import models

# Create your models here.

# 一级菜单数据表
class Menu(models.Model):
    name = models.CharField(max_length=32)   # 一级菜单名
    icon = models.CharField(max_length=32,null=True,blank=True)  # 一级菜单图标
    weight = models.IntegerField(default=100)  #控制菜单排序的,权重值越大,菜单展示越靠前

    def __str__(self):
        return self.name


# 权限
class Permission(models.Model):
    title = models.CharField(max_length=32)  # 用户权限名
    url = models.CharField(max_length=32)  # 权限对应访问路径
    menus = models.ForeignKey('Menu',null=True,blank=True)  # 此权限是否属于菜单权限

    def __str__(self):
        return self.title


# 用户表
class UserInfo(models.Model):
    username = models.CharField(max_length=32)  #用户姓名
    password = models.CharField(max_length=32)  #用户密码
    roles = models.ManyToManyField('Role')  #用户对应角色
    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

4.3 注入权限(封装的session功能)

from rbac import models

def init_permission(request,user_obj):
    # 登录成功之后,将该用户所有的权限(url)全部注入到session中
    # 根据用户名查询用户对应权限注入session值
    permission_list = models.Role.objects.filter(userinfo__username=user_obj.username) \
        .values('permissions__url', 'permissions__title', 'permissions__menus__pk','permissions__menus__name','permissions__menus__icon','permissions__menus__weight').distinct()
    request.session['permission_list'] = list(permission_list)
        
'''
	permissions__url  用户角色可访问路径
	permissions__title  用户权限名
	permissions__menus__pk  用户一级菜单权限主键
	permissions__menus__name  用户一级菜单权限名
	permissions__menus__icon  用户一级菜单图标名
	permissions__menus__weight  用户一级菜单权重  根据菜单权重为菜单排序
'''
        
	menu_dict = {}

    for i in permission_list:
        # 如果用户有一级菜单权限
        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')}
                )
            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')},
                    ],
                }
    '''
        {
            # 1-- 一级菜单的id
            1:{
                'name':'业务系统',
                'icon':'fa fa-xx',
                'children':[
                    {'title':'客户管理','url':'/customer/list/',},
                ]
            },
            
            2:{
                'name':'财务系统',
                'icon':'fa fa-xx2',
                'weight':100,
                'children':[
                    {'title':'缴费展示','url':'/payment/list/',},
                    
                ]
            }
        }

    '''
    request.session['menu_dict'] = menu_dict

4.4 认证视图相关

from django.shortcuts import HttpResponse,redirect,render
from rbac import models
from rbac.utils.permission_injection import init_permission
def login(request):

    if request.method == 'GET':
        return render(request,'login.html')
    else:
        uname = request.POST.get('username')
        pwd = request.POST.get('password')

        user_obj = models.UserInfo.objects.filter(username=uname,password=pwd)
        if user_obj:
            user_obj = user_obj.first()
            # 登录认证标识
            request.session['is_login'] = True

            # 权限注入
            init_permission(request,user_obj)
            return redirect('index')
        else:
            return redirect('login')

# 首页        
def index(request):   
     return render(request,'index.html')

4.5 rbac模块下定制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'),]
        # 权限认证白名单
        permission_white_list = [reverse('index'), '/admin/*'] #/admin/login/?next=/admin/

        # 登录认证
        path = request.path
        if path not in white_list:
            is_login = request.session.get('is_login')
            if not is_login:
                return redirect('login')
            # 权限认证
            permission_list = request.session.get('permission_list')
            for white_path in permission_white_list:
                if re.match(white_path,path):
                    break
            else:
                for i in permission_list:
                    reg = r"^%s$"%i['permissions__url']
                    if re.match(reg,path):
                        break
                else:
                    return HttpResponse('您配吗??!')

4.6 动态生成左侧菜单

rbac应用---->templatetags---->mytags.py

from django import template
import re
from collections import OrderedDict
register = template.Library()

@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_order_dict = 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):
                v['class'] = ''
                i['class'] = 'active'

    menu_data = {'menu_order_dict':menu_order_dict}
    return menu_data

组件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div class="left-menu">
    <div class="menu-body">
        <div class="multi-menu">

            {% for k,v in menu_order_dict.items %}
                <div class="item">
                    <div class="title"><i class="{{ v.icon }}"></i>{{ v.name }}</div>
                    <div class="body {{ v.class }}">

                        {% for child in v.children %}
                            <a href="{{ child.url }}" class="{{ child.class }}">{{ child.title }}</a>
                        {% endfor %}
                        
                    </div>
                </div>
 
            {% endfor %}

        </div>
    </div>
</div>
</body>
</html>

模板

...
# 导入应用自定义组件
	{% load mytag %}
	{% menu request %}
...
posted @ 2023-03-28 20:06  河图s  阅读(75)  评论(0)    收藏  举报