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

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

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

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

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

RBAC权限设计:

一张图搞定RBAC用户权限设计:

权限设计的大致步骤:

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

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表

rbac_role表

rbac_role_permissions表

rbac_userinfo表

rbac_userinfo_roles表

rbac_menu表

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

百万年薪python之路 -- RBAC角色权限设计的更多相关文章

  1. RBAC角色权限设计思路

    1 设计思路 为了设计一套具有较强可扩展性的用户认证管理,需要建立用户.角色和权限等数据库表,并且建立之间的关系,具体实现如下. 1.1 用户 用户仅仅是纯粹的用户,用来记录用户相关信息,如用户名.密 ...

  2. 百万年薪python之路 -- MySQL数据库之 用户权限

    MySQL用户授权 (来自于https://www.cnblogs.com/dong-/p/9667787.html) 一. 对新用户的增删改 1. 增加用户 : ①. 指定某一个用户使用某一个ip登 ...

  3. 百万年薪python之路 -- 数据库初始

    一. 数据库初始 1. 为什么要有数据库? ​ 先来一个场景: ​ 假设现在你已经是某大型互联网公司的高级程序员,让你写一个火车票购票系统,来hold住十一期间全国的购票需求,你怎么写? 由于在同一时 ...

  4. 百万年薪python之路 -- 并发编程之 协程

    协程 一. 协程的引入 本节的主题是基于单线程来实现并发,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发,为此我们需要先回顾下并发的本质:切换+保存状态 cpu正在运行一个任务,会在两 ...

  5. 百万年薪python之路 -- 面向对象之继承

    面向对象之继承 1.什么是面向对象的继承 继承(英语:inheritance)是面向对象软件技术当中的一个概念. 通俗易懂的理解是:子承父业,合法继承家产 专业的理解是:子类可以完全使用父类的方法和属 ...

  6. 百万年薪python之路 -- re模块

    re模块 re模块是python用来描述正则表达式的一个模块. 正则表达式本身也和python没有什么关系,就是匹配字符串内容的一种规则. 官方定义:正则表达式是对字符串操作的一种逻辑公式,就是用事先 ...

  7. 百万年薪python之路 -- JS基础介绍及数据类型

    JS代码的引入 方式1: <script> alert('兽人永不为奴!') </script> 方式2:外部文件引入 src属性值为js文件路径 <script src ...

  8. 百万年薪python之路 -- 前端CSS样式

    CSS样式 控制高度和宽度 width宽度 height高度 块级标签能设置高度和宽度,而内联标签不能设置高度和宽度,内联标签的高度宽度由标签内部的内容来决定. 示例: <!DOCTYPE ht ...

  9. 百万年薪python之路 -- MySQL数据库之 Navicat工具和pymysql模块

    一. IDE工具介绍(Navicat) 生产环境还是推荐使用mysql命令行,但为了方便我们测试,可以使用IDE工具,我们使用Navicat工具,这个工具本质上就是一个socket客户端,可视化的连接 ...

随机推荐

  1. [C++] C++中的常用库

    转载自:C++常用库 C++ 资源大全 关于 C++ 框架.库和资源的一些汇总列表,内容包括:标准库.Web应用框架.人工智能.数据库.图片处理.机器学习.日志.代码分析等. 标准库 C++标准库,包 ...

  2. spring项目与logstash和Elasticsearch整合

    原创/朱季谦   最近在做一个将项目日志通过logstash传到Elasticsearch的功能模块,经过一番捣鼓,终于把这个过程给走通了,根据自己的经验,做了这篇总结文章,希望可以给各位玩logst ...

  3. CocosCreator实现动物同化

    获取源码 关注微信公众号『一枚小工 』,发送『动物同化 』获取完整游戏源码. 游戏玩法 游戏目标是将游戏区域的动物全部同化成同一种动物.游戏从左上角开始,从右边点击需要变成的目标动物头像,如果被同化动 ...

  4. Jib构建镜像的问题分析(Could not find or load main class ${start-class})

    问题简述 通过Jib插件将SpringBoot工程制作成Docker镜像成功,但是运行镜像的时候报错(Could not find or load main class ${start-class}) ...

  5. JAVA设计模式---单例模式篇

    单例模式(singleton):是JAVA中最简单的一种设计模式,属于创建型模式.所谓单例,就是整个程序有且仅有一个实例. 特点: 构造方法私有化 在本类中实例化一个对象作为本类的属性 对外提供一个访 ...

  6. 【django】ajax,上传文件,图片预览

    1.ajax 概述: AJAX = 异步 JavaScript 和 XML. AJAX 是一种用于创建快速动态网页的技术. 通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新.这意味 ...

  7. linux 查看文件大小命令

    1.# ls -l (k) ls -l total -rw-r----- root root Oct : catalina.--.log -rw-r----- root root Oct : cata ...

  8. Java中的static(1)【持续更新】——关于Eclipse的No enclosing instance of type ... 错误的理解和改正

    No enclosing instance of type SomeClass is accessible. Must qualify the allocation with an enclosing ...

  9. linux mint 19.2与Windows 10 双系统硬盘安装与卸载

    安装linux mint 和win10双系统: 1.win10系统下如果没有空闲分区,请从容量较大的分区用partition manager在选中的较大的分区下,调整大小.此步骤最好在pe下的part ...

  10. Docker 第一个HelloWorld镜像

    Docker 创建第一个HelloWorld镜像: 创建Dockerfile FROM alpine CMD "echo" "Hello World!" 通过D ...