RABC权限控制(二级菜单实现)

Posted leixiaobai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RABC权限控制(二级菜单实现)相关的知识,希望对你有一定的参考价值。

  目前大部分系统由于用户体验,基本上菜单不会做的很深,以二级菜单为例,做了一个简单的权限控制实现,可精确到按钮级别(基于django),下面具体看看实现

1.表结构的设计

无论开发什么都需要先梳理清楚需求,然后再考虑表结构,这里先来说说大致的表结构组成,注意,我的权限控制是通过url做的,所以控制的核心就在于控制url

表字段介绍设计如下:

权限表
    url   # 权限
    title  #权限的标题,左侧展示,代表的功能(因为不可能展示url吧)
    menu    # 所属的一级菜单,外键关联一级菜单
    parent    # 二级菜单下的子权限,类似xx列表,旗下的增删改就是子权限,所以这个需要外键自关联当前表
    url_name    # url分发的别名,主要是用于按钮级别权限控制,也是为了之后的扩展
    icon    # 二级菜单的图标

角色表
    name    # 角色的名称
    permissions    # 与权限表多对多的关系,一个角色可以有多个权限,一个权限也可以给多个角色

用户表
    name    # 用户名
    pwd      # 加密后的密码
    roles     # 与角色表是多对多的关系

一级菜单表
    title    # 一级菜单的标题
    icon    # 一级菜单的图标
    weight    # 一级菜单的权重,通过权重控制一级菜单的顺序,权重最大在最上面

整体逻辑就是创了用户后,可以给该用户分配角色,由于角色拥有特定权限,所以用户久拥有了相应的权限

代码如下:

from django.db import models


class Menu(models.Model):
    """
    一级菜单表
    """
    title = models.CharField(max_length=32, verbose_name=一级菜单)
    icon = models.CharField(max_length=32, verbose_name=图标, null=True, blank=True)
    weight = models.IntegerField(verbose_name=菜单权重, default=1)

    def __str__(self):
        return self.title


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(max_length=32, verbose_name=标题)
    url = models.CharField(max_length=32, verbose_name=权限)
    icon = models.CharField(max_length=32, verbose_name=图标, null=True, blank=True)
    menu = models.ForeignKey(to=Menu, verbose_name=所属菜单, on_delete=models.CASCADE, null=True, blank=True)
    url_name = models.CharField(max_length=32, verbose_name=url别名, null=True, blank=True)
    parent = models.ForeignKey(self, on_delete=models.CASCADE, verbose_name=父级菜单, null=True, blank=True)
    # is_menu = models.BooleanField(default=False, verbose_name=‘是否是菜单‘)

    class Meta:
        verbose_name_plural = "权限表"
        verbose_name = 权限表

    def __str__(self):
        return self.title


class Role(models.Model):
    name = models.CharField(max_length=32, verbose_name=角色名称)
    permissions = models.ManyToManyField(to=Permission, verbose_name=角色所拥有的权限, blank=True)

    def __str__(self):
        return self.name


class User(models.Model):
    """
    用户表
    """
    name = models.CharField(max_length=32, verbose_name=用户名)
    pwd = models.CharField(max_length=32, verbose_name=密码)
    roles = models.ManyToManyField(to=Role, verbose_name=用户所拥有的角色, blank=True)

    def __str__(self):
        return self.name

四个模型,6张表,因为用户和角色,角色和权限都是多对多的关系,所以django会自动生成两张表记录多对多的关系.

 

2.成功登录后初始化用户信息(合适的数据结构的设计非常重要也比较难)

将表数据录入后,就表示用户拥有了不同权限,因此我们以用户身份去登录平台,首先登陆平台需要登录,因此在中间件中需要先设置白名单,避免登录,注册等url也被权限控制拦截,登录认证成功后,就将相关信息存入用户的session中,这里来详细说明下存了哪些信息,直接看代码,我在代码里面进行步骤和备注说明

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: Xiaobai Lei
from rbac.models import Role


def initial_session(user_obj, request):
    """
    将当前登录人的信息记录到session中
    :param user_obj: 用户对象
    :param request:
    :return:
    """
    # 1.查询当前登录人的权限列表,取出相关的信息
    permissions = Role.objects.filter(user=user_obj).values(permissions__url,
                                                            permissions__pk,
                                                            permissions__title,
                                                            permissions__icon,
                                                            permissions__parent_id,
                                                            permissions__url_name,
                                                            permissions__menu__pk,
                                                            permissions__menu__title,
                                                            permissions__menu__weight,
                                                            permissions__menu__icon).distinct()
    # 2.保存登录人的相关信息(权限列表,别名列表和权限菜单字典)
    """
    权限列表,以字典形式存储当前用户的每一个权限信息,数据格式如下:
    permission_list = [
        ‘id‘: 1, ‘url‘: ‘/custmer/list/‘, ‘title‘: ‘客户列表‘, ‘parent_id‘: None,
        ‘id‘: 2, ‘url‘: ‘/custmer/add/‘, ‘title‘: ‘客户添加‘, ‘parent_id‘: 1,
    ]
    原先简单设计只是列表保存当前用户的所有url,后面发现访问子权限(比如客户添加)时,依旧需要左侧客户列表展示,
    所以需要用到父权限(客户列表)的信息,而且为了更多扩展,所以采用了列表嵌套字典的形式保存了较多数据
    """
    # 权限列表,主要用于用户的权限校验
    permission_list = []
    # 别名列表,主要用于按钮级别的控制,比如客户添加的按钮
    permission_url_names = []
    """
    权限菜单字典,数据格式如下:
    permission_menu_dict = 
        ‘一级菜单id‘: 
            ‘menu_title‘: ‘信息管理‘,
            ‘menu_icon‘: ‘一级菜单图标‘,
            ‘menu_weight‘: ‘一级菜单的权重‘,
            ‘menu_children‘: [
                    ‘id‘: 1, ‘url‘: ‘/custmer/list/‘, ‘title‘: ‘客户列表‘, ‘parent_id‘: None,
                ]
        
    
    注意:menu_chidren只保存的是二级菜单(如客户列表),通过这个数据结构就可以很清晰的看到层级关系了,如果还有一级菜单
    的话,那么就需要在客户列表字典结构中再加入一个node_children:[],就是一个不断循环嵌套的过程,你懂的
    """
    # 权限菜单字典,主要用于左侧菜单的数据展示
    permission_menu_dict = 

    # 循环获取上面提及的数据结构
    for item in permissions:
        permission_list.append(
            url: item[permissions__url],
            id: item[permissions__pk],
            parent_id: item[permissions__parent_id],
            title: item[permissions__title],
        )
        permission_url_names.append(item[permissions__url_name])
        menu_id = item[permissions__menu__pk]
        # 只有二级菜单才被加入,也就是父权限(如客户列表)
        if menu_id:
            # 如果字典中已经存在了菜单id就直接在一级菜单的menu_chidren下追加,没有则先新建
            if menu_id not in permission_menu_dict:
                permission_menu_dict[menu_id] = 
                    menu_title: item[permissions__menu__title],
                    menu_icon: item[permissions__menu__icon],
                    menu_weight: item[permissions__menu__weight],
                    menu_children: [
                        
                          title:  item[permissions__title],
                          url:  item[permissions__url],
                          icon:  item[permissions__icon],
                          id:  item[permissions__pk],
                        ,
                    ]
                
            else:
                permission_menu_dict[menu_id][menu_children].append(
                    title: item[permissions__title],
                    url: item[permissions__url],
                    icon: item[permissions__icon],
                    id:  item[permissions__pk],
                )

    # 根据一级菜单权重进行重新排序
    permission_menu_dict_new = 
    for i in sorted(permission_menu_dict, key=lambda x: permission_menu_dict[x][menu_weight], reverse=True):
        permission_menu_dict_new[i] = permission_menu_dict[i]

    # 将用户的权限列表和权限菜单列表注入session中
    request.session[permission_list] = permission_list
    request.session[permission_url_names] = permission_url_names
    request.session[permission_menu_dict] = permission_menu_dict_new

 

3.权限校验(采用django自定义中间件)

由于每次访问都是需要进行权限校验的,因此就放在了中间件中,之前也提到过,在权限校验之前你必须是登录成功的用户,因此中间件中还加入了用户认证,具体请见如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: Xiaobai Lei
import re

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import (
    redirect, reverse, HttpResponse
)

from rbac.models import Permission
# 白名单列表
WHITE_URL_LIST = [
    r^/login/$,
    r^/logout/$,
    r^/reg/$,
    r^/favicon.ico$,
    r^/admin/.*,
]


class PermissionMiddleware(MiddlewareMixin):
    """权限验证中间件"""

    def process_request(self, request):
        # 1.当前访问的url
        current_path = request.path_info

        # 2.白名单判断,如果在白名单的就直接放过去
        for path in WHITE_URL_LIST:
            if re.search(path, current_path):
                return None

        # 3.检验当前用户是否登录
        user_id = request.session.get(user_id)
        if not user_id:
            return redirect(reverse(login))

        # 面包屑导航栏层级记录,默认首页为第一位,主要存储title(展示在页面用)和url(用户点击后可直接跳转到相应页面)
        request.breadcrumb_list = [
            
                title: 首页,
                url: /index/,
            
        ]
        # 4.获取用户权限信息并进行校验
        permission_list = request.session.get(permission_list)
        for item in permission_list:
            # 由于url的是以正则形式存储,因此采用正则与当前访问的url进行完全匹配,如果符合则证明有权限
            if re.search(^$.format(item[url]), current_path):
                # 将当前访问路径的所属菜单pk记录到show_id中,用户访问子权限时依旧会显示父权限(二级菜单)
                request.show_id = item[parent_id] or item[id]
                # 将当前访问的父子信息记录到breadcrumb_list中(面包屑导航栏)
                # 如果是子权限的话,就根据父权限id查出父权限信息,将父权限和子权限都记录下来
                parent_obj = Permission.objects.filter(pk=item[parent_id]).first()
                if item[parent_id]:
                    request.breadcrumb_list.extend([
                        
                            title: parent_obj.title,
                            url: parent_obj.url,
                        ,
                        
                            title: item[title],
                            url: item[url],
                        ])
                else:
                    # 排除首页,因为首页初始化就存在了
                    if item[title] != 首页:
                        request.breadcrumb_list.append(
                            title: item[title],
                            url: item[url],
                        )
                return None
        else:
            return HttpResponse("无此权限")

 

4.自定义通用模板(inclusion_tag)

通过上面的校验后,如果该用户有权限则进入系统,并且展示左侧菜单,但在此时想一下,如果是直接展示的话那么就意味着每一个视图函数(django业务逻辑处理相关)都需要返回菜单的数据给模板层,因此在这里就用到了inclusion_tag通用模板,注意:需要新建一个包,名称必须是templatetags,在包下我新建了一个my_tag.py文件,存放一下内容

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: Xiaobai Lei
from django.template import Library


register = Library()


# 获取左侧菜单数据给menu.html然后进行展示
@register.inclusion_tag(rbac/menu.html)
def get_menu_displays(request):
    # 获取菜单的字典数据
    permission_menu_dict = request.session.get(permission_menu_dict)
    # 循环获取每个菜单信息
    for menu in permission_menu_dict.values():
        # 默认二级菜单都是隐藏状态
        menu[class] = hide
        # 循环获取每个二级菜单信息
        for reg in menu[menu_children]:
            # if re.search("^$".format(reg[‘url‘]), request.path):
            # 在中间件处理时就已经将父子权限的show_id都变成了父权限的id,以此来表示无论操作哪一个,左侧父权限菜单都是被选中状态
            if request.show_id == reg[id]:
                reg[class] = active
                # 显示二级菜单
                menu[class] = ‘‘
    return permission_menu_dict: permission_menu_dict


@register.filter
def url_is_permission(url, request):
    """判断当前按钮url是否在权限列表"""
    permission_url_names = request.session.get(permission_url_names)
    return url in permission_url_names

 

5.menu.html(循环展示菜单)

<div class="multi-menu">
    % for item in permission_menu_dict.values %
        <div class="item">
            <div class="title"><i style="margin-right: 3px" class="fa  item.menu_icon "></i> item.menu_title </div>
            <div class="body   item.class ">
                % for menu_chidren in item.menu_chidren %
                    <a class=" menu_chidren.class " href=" menu_chidren.url ">
                    <span class="icon-wrap"><i style="margin-right: 3px" class="fa  menu_chidren.icon "></i></span> menu_chidren.title </a>
                % endfor %
            </div>
        </div>
    % endfor %
</div>

 

6.最后需要在业务的html中应用自定义的inclusion_tag

 % load my_tag %
 % get_menu_displays request %

 

7.按钮级别控制(针对不同二级菜单页面进行按钮控制),如下

% if ‘customer_edit‘|url_is_permission:request %
     <a style="color: #333333;" href="/customer/edit/ row.id /">
        <i class="fa fa-edit" aria-hidden="true"></i></a>
% endif %

 

至此,权限大体开发完成,目前数据还需要自己去admin管理后台录入,下一篇我会继续说一下开发权限管理的功能,这样就能直接在系统上进行用户,角色和权限的自由分配了,到时会将权限和CRM项目合为一体分享源码.

以上是关于RABC权限控制(二级菜单实现)的主要内容,如果未能解决你的问题,请参考以下文章

RABC --权限控制解读

vue实现菜单权限控制

ecshop 后台添加新菜单 以及 权限控制

RBAC权限分配

SpringBoot整合SpringSecurity实现权限控制:菜单管理

自定义标签 + shiro 实现权限细粒度控制