关于RBAC权限组件的总结(包含数据库表的设计以及操作)

这篇文章是参考武sir的rbac博客进行整理,博客地址为:https://www.cnblogs.com/wupeiqi/articles/9178982.html
gitee代码地址为:https://gitee.com/sunweigogogo/luffy_permission/

rbac数据库表结构以及admin后台配置

from django.db import models


class Permission(models.Model):
    """
    权限表
    """
    title = models.CharField(verbose_name='标题', max_length=32)
    url = models.CharField(verbose_name='含正则的URL', max_length=128)

    def __str__(self):
        return self.title


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

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户表
    """
    name = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    email = models.CharField(verbose_name='邮箱', max_length=32)
    roles = models.ManyToManyField(verbose_name='拥有的所有角色', to='Role', blank=True)

    def __str__(self):
        return self.name

想要在django-admin后台直接配置rbac应用中的数据库表,需要如下配置:

  • rbac/admin.py文件

      from django.contrib import admin
      from rbac import models
    
      admin.site.register(models.Permission)
      admin.site.register(models.UserInfo)
      admin.site.register(models.Role)
    
  • 创建超级用户

    python manage.py createsuperuser
    
  • 录入用户权限以及角色相关信息

    修改订单等权限(url)是web业务中生成的

    image
    image
    image

备注:通过上面的操作,已经手动将业务访问路径,也就是我们说的权限或者叫URL,手动录入到后台系统当中了,接下来就是通过代码去对用户的权限进行操作,需要不断的调用orm去操作。

第一部分:快速实现简单权限控制之权限校验

1. 用户登陆

  • luffy_permission/web/views/account.py

    def login(request):
        if request.method == "GET":
            return render(request, 'login.html')
        username = request.POST.get("username")
        password = request.POST.get("password")
        curren_user_obj = models.UserInfo.objects.filter(name=username, password=password).first()
        # curren_user_obj = models.UserInfo.objects.filter(**request.POST)
        if not curren_user_obj:
            return render(request, 'login.html', {'msg': "用户名或者密码错误"})
        # 查询用户的角色,通过用户的角色去查询用户所有的权限,存入session中(因为rbac中用户表、角色表、权限表中都存在m2m字段,所以需要进行连表查询)
        # 方式1:通过permission来反向过滤查找用户拥有的权限
        permission_queryset1 = models.Permission.objects.filter(role__userinfo=curren_user_obj).distinct()
        # for item in permission_queryset1:
        #     print(item.url)
        # 通过用户正向查询其拥有的权限,因为是多对多,部分角色可能不具备任何权限,所以需要permissions__isnull=False过滤其中角色权限为空的数据,distinct去重
        permission_queryset = curren_user_obj.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                             "permissions__url").distinct()
        permission_list = [item["permissions__url"] for item in permission_queryset ]
        # 将登陆用户所拥有的权限存入session中,便于后续中间件中利用此数据进行权限校验
        request.session["luffy_permission_url_list_key"] = permission_list
    
        # 至此登陆成功,重定向
        return redirect('/customer/list/')
    

2. 加载中间件进行权限控制

  • luffy_permission/web/views/account.py

    import re
    
    from django.utils.deprecation import MiddlewareMixin
    from django.shortcuts import HttpResponse
    
    
    class AuthMiddlewareMixin(MiddlewareMixin):
        def process_request(self, request):
            """
            当请求刚进入的时候被触发
            :param request:
            :return:
            """
            """
            1.获取当前用户请求的url
            2.获取当前用户在session中存储的权限列表 ['/customer/list/', '/customer/add/']
            3.权限进行匹配
            """
    
            # http://127.0.0.1:8000/customer/list/          -> /customer/list/
            # http://127.0.0.1:8000/customer/list/?age=18   -> /customer/list/
            current_url = request.path_info
            permission_list = request.session.get("luffy_permission_url_list_key")
    
            if not permission_list:
                return HttpResponse("未获取到任何用户信息,请登陆!!!")
            flag = False
            for url in permission_list:
                reg = "^%s$" % url
                if re.match(reg, current_url):
                    flag = True
                    break
            if not flag:
                return HttpResponse("无权访问")
    
    

3. 代码优化

  • 封装以及配置优化
    image

将初始化权限以及中间件全部放到rbac目录中,降低与业务的耦合性

第二部分:动态菜单

1. 一级菜单

  • 数据库字段变更

    class Permission(models.Model):
      """
      权限表
      """
      title = models.CharField(verbose_name='标题', max_length=32)
      url = models.CharField(verbose_name='含正则的URL', max_length=128)
      # 新增
      is_menu = models.BooleanField(verbose_name="是否可以做菜单", default=False)
      # 新增
      icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)
    
      def __str__(self):
          return self.title
    
  • 获取菜单信息并保存到session

    def init_permission(curren_user_obj, request):
        # 权限信息初始化
        # 查询用户的角色,通过用户的角色去查询用户所有的权限,存入session中(因为rbac中用户表、角色表、权限表中都存在m2m字段,所以需要进行连表查询)
        # 方式1:通过permission来反向过滤查找用户拥有的权限
        permission_queryset1 = models.Permission.objects.filter(role__userinfo=curren_user_obj).distinct()
        # for item in permission_queryset1:
        #     print(item.url)
        # 通过用户正向查询其拥有的权限,因为是多对多,部分角色可能不具备任何权限,所以需要permissions__isnull=False过滤其中角色权限为空的数据,distinct去重
        permission_queryset = curren_user_obj.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                             "permissions__title",
                                                                                             "permissions__is_menu",
                                                                                             "permissions__icon",
                                                                                             "permissions__url").distinct()
        # 获取权限以及菜单信息
        menu_list = []
        permission_list = []
        for item in permission_queryset:
            permission_list.append(item["permissions__url"])
            if item['permissions__is_menu']:
                temp = {
                    'title': item['permissions__title'],
                    'icon': item['permissions__icon'],
                    'url': item['permissions__url'],
                }
                menu_list.append(temp)
        # 将登陆用户所拥有的权限存入session中,便于后续中间件中利用此数据进行权限校验
        request.session[settings.SESSION_PERMISSION_KEY] = permission_list
        request.session[settings.SESSION_MENU_KEY] = menu_list
    
  • html模板中显示菜单信息(从session中获取)

    这块使用inclusion_tag进行html代码片段的生成

    #rbac/templatetags/rbac.py
    from django.template import Library
    from django.conf import settings
    register = Library()
    
    
    @register.inclusion_tag('rbac/static_menu.html')
    def static_menu(request):
        """
        创建一级菜单
        :return:
        """
        menu_list = request.session[settings.SESSION_MENU_KEY]
        curren_url = request.path_info
    
        return {'menu_list': menu_list, 'curren_url': curren_url}
    
    #templates/rbac/static_menu.html
    {% for item in menu_list %}
        <a href="{{ item.url }}" class="{% if curren_url == item.url %}active{% endif %}">
            <span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</a>
    {% endfor %}
    
    
    #业务中直接进行inclusion_tag的引用,web/templates/layout.html
    {% load rbac %}
    <div class="static-menu">
       {% static_menu request %}
    </div>
    
    

2. 二级菜单

就是构造一个菜单,然后遍历

  • 数据库字段变更
    #新增类
    class Menu(models.Model):
        """
        菜单表
        """
        title = models.CharField(verbose_name='一级菜单名称', max_length=32)
        icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)
    
        def __str__(self):
            return self.title
    
    class Permission(models.Model):
        """
        权限表
        """
        title = models.CharField(verbose_name='标题', max_length=32)
        url = models.CharField(verbose_name='含正则的URL', max_length=128)
        # 注释下面两行
        # is_menu = models.BooleanField(verbose_name="是否可以做菜单", default=False)
        # icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)
        # 新增    
        menu = models.ForeignKey(verbose_name="所属菜单", to='Menu', null=True, blank=True, help_text='null表示不是菜单;非null表示是二级菜单', on_delete=models.SET_NULL)
    
        def __str__(self):
            return self.title
    
  • 获取菜单信息并保存到session
    from rbac import models
    from django.conf import settings
    
    
    def init_permission(curren_user_obj, request):
        # 权限信息初始化(一级菜单)
        # 查询用户的角色,通过用户的角色去查询用户所有的权限,存入session中(因为rbac中用户表、角色表、权限表中都存在m2m字段,所以需要进行连表查询)
        # 方式1:通过permission来反向过滤查找用户拥有的权限
        permission_queryset1 = models.Permission.objects.filter(role__userinfo=curren_user_obj).distinct()
        # for item in permission_queryset1:
        #     print(item.url)
        # 通过用户正向查询其拥有的权限,因为是多对多,部分角色可能不具备任何权限,所以需要permissions__isnull=False过滤其中角色权限为空的数据,distinct去重
        # permission_queryset = curren_user_obj.roles.filter(permissions__isnull=False).values("permissions__id",
        #                                                                                      "permissions__title",
        #                                                                                      "permissions__is_menu",
        #                                                                                      "permissions__icon",
        #                                                                                      "permissions__url").distinct()
        # 权限信息初始化(二级菜单)
        permission_queryset = curren_user_obj.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                             "permissions__title",
                                                                                             "permissions__menu_id",
                                                                                             "permissions__menu__title",
                                                                                             "permissions__menu__icon",
                                                                                             "permissions__url").distinct()
        # 获取权限以及菜单信息(一级菜单)
        # menu_list = []
        # permission_list = []
        # for item in permission_queryset:
        #     permission_list.append(item["permissions__url"])
        #     if item['permissions__is_menu']:
        #         temp = {
        #             'title': item['permissions__title'],
        #             'icon': item['permissions__icon'],
        #             'url': item['permissions__url'],
        #         }
        #         menu_list.append(temp)
    
        # 获取权限以及生成二级菜单信息(二级菜单)
        permission_list = []
        menu_dict = {}
        for item in permission_queryset:
            permission_list.append(item["permissions__url"])
    
            menu_id = item["permissions__menu_id"]
            if not menu_id:
                continue
            node = {'title': item['permissions__menu__title'], 'url': item['permissions__url']}
            if menu_id in menu_dict:
                menu_dict[menu_id]['children'].append(node)
            else:
                menu_dict[menu_id] = {
                    'title': item['permissions__title'],
                    'icon': item['permissions__menu__icon'],
                    'children': [node, ]
                }
        print(menu_dict)
        # 将登陆用户所拥有的权限存入session中,便于后续中间件中利用此数据进行权限校验
        request.session[settings.SESSION_PERMISSION_KEY] = permission_list
        request.session[settings.SESSION_MENU_KEY] = menu_dict
    
  • html模板中显示菜单信息(从session中获取)

    这块使用inclusion_tag进行html代码片段的生成

    # rbac/templatetags/rbac.py
    @register.inclusion_tag('rbac/multi_menu.html')
    def multi_menu(request):
        """
        创建二级菜单
        :return:
        """
        menu_dict = request.session[settings.SESSION_MENU_KEY]
        # 对字典的key进行排序
        key_list = sorted(menu_dict)
        # 空的有序字典
        ordered_dict = OrderedDict()
    
        for key in key_list:
            val = menu_dict[key]
            val['class'] = 'hide'
            for per in val['children']:
                regex = "^%s$" % (per['url'],)
                if re.match(regex, request.path_info):
                    per['class'] = 'active'
                    val['class'] = ''
            ordered_dict[key] = val
        print("111", ordered_dict)
        return {'menu_dict': ordered_dict}
    
    # rbac/templates/rbac/multi_menu.html
    <div class="multi-menu">
        {% for item in menu_dict.values %}
            <div class="item">
                <div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
                <div class="body  {{ item.class }}">
                    {% for per in item.children %}
                        <a class="{{ per.class }}" href="{{ per.url }}">{{ per.title }}</a>
                    {% endfor %}
                </div>
            </div>
        {% endfor %}
    </div>
    
    

3. 非菜单默认选中

点击非菜单的按钮时候,比如这个 /customer/delete/2 对应的按钮,那么左侧的菜单栏,应该会要默认有选中状态

  • 数据库字段的修改

    这样,每一个url权限会对应一个pid,会显示它的父菜单

    class Permission(models.Model):
        """
        权限表
        """
        title = models.CharField(verbose_name='标题', max_length=32)
        url = models.CharField(verbose_name='含正则的URL', max_length=128)
        # is_menu = models.BooleanField(verbose_name="是否可以做菜单", default=False)
        # icon = models.CharField(verbose_name="图标", max_length=32, null=True, blank=True)
        menu = models.ForeignKey(verbose_name="所属菜单", to='Menu', null=True, blank=True, help_text='null表示不是菜单;非null表示是二级菜单',
                                 on_delete=models.SET_NULL)
        # 新增pid字段,关联Permission
        pid = models.ForeignKey(verbose_name="关联的权限", to='Permission',
                                null=True, blank=True,
                                on_delete=models.SET_NULL,
                                related_name='parents',
                                help_text='对于非菜单权限需要选择一个可以成为菜单的权限,用户做默认展开和选中菜单')
    
        def __str__(self):
            return self.title
    
    
  • 初始化权限,将其放入session中

    #rbac/service/init_permission.py
    # 非菜单默认选中
        permission_list = []
        menu_dict = {}
        for item in permission_queryset:
            permission_list.append({"id": item["permissions__id"], "url": item['permissions__url'],
                                    "pid": item['permissions__pid']})
    
            menu_id = item["permissions__menu_id"]
            if not menu_id:
                continue
            node = {"id": item['permissions__id'], 'title': item['permissions__menu__title'],
                    'url': item['permissions__url']}
            if menu_id in menu_dict:
                menu_dict[menu_id]['children'].append(node)
            else:
                menu_dict[menu_id] = {
                    'title': item['permissions__menu__title'],
                    'icon': item['permissions__menu__icon'],
                    'children': [node, ]
                }
        # 将登陆用户所拥有的权限存入session中,便于后续中间件中利用此数据进行权限校验
        request.session[settings.SESSION_PERMISSION_KEY] = permission_list
        request.session[settings.SESSION_MENU_KEY] = menu_dict
    
    #查看具体的数据:
    #permission_list如下
      [
       {'url': '/customer/list/', 'id': 1, 'pid': None},
       {'url': '/customer/add/', 'id': 2, 'pid': 1},
       {'url': '/customer/list/(?P<cid>\\d+)/', 'id': 3, 'pid': 1},
       {'url': '/customer/edit/(?P<cid>\\d+)/', ' id': 4, ' pid': 1}, {' url': ' /customer/import/', ' id': 5, ' pid': 1}, {' url': ' /customer/tpl/', ' id': 6, ' pid': 1}, {' url': ' /payment/list/', ' id': 7, ' pid': None},
       {'url': '/payment/add/', 'id': 8, 'pid': 7},
       {'url': '/payment/del/(?P<pid>\\d+)/', 'id': 9, 'pid': 7},
       {'url': '/payment/edit/<?P<pid>\\d+/', 'id': 10, 'pid': 7}
      ]
    
    
    #menu_dict如下
    {
      1: {
          'icon': 'fa-times-rectangle',
          'title': '信息管理',
          'children': [ 
              {'url': '/customer/list/', 'id': 1, 'title': '信息管理'}
           ]
         },
      2: {
          'icon': ' fa-window-close-o',
          'title': '用户管理',
          'children': [ 
              {'url': '/payment/list/', ' id': 7, ' title': ' 用户管理'}
           ]
         }
    }
    
  • 初始化权限,将其放入session中

    #rbac/templatetags/rbac.py
    
    from django.template import Library
    from django.conf import settings
    import re
    from collections import OrderedDict
    register = Library()
    @register.inclusion_tag('rbac/multi_menu.html')
    def multi_menu(request):
        """
        创建二级菜单
        :return:
        """
        menu_dict = request.session[settings.SESSION_MENU_KEY]
        # 对字典的key进行排序
        key_list = sorted(menu_dict)
        # 空的有序字典
        ordered_dict = OrderedDict()
    
        for key in key_list:
            val = menu_dict[key]
            val['class'] = 'hide'
            for per in val['children']:
                # regex = "^%s$" % (per['url'],)
                # if re.match(regex, request.path_info):
                #     per['class'] = 'active'
                #     val['class'] = ''
                if per['id'] == request.current_selected_permission:
                    per['class'] = 'active'
                    val['class'] = ''
            ordered_dict[key] = val
        print("222", ordered_dict)
        return {'menu_dict': ordered_dict}
    
    
  • html代码片段生成

    #rbac/templates/rbac/multi_menu.html
    <div class="multi-menu">
        {% for item in menu_dict.values %}
            <div class="item">
                <div class="title"><span class="icon-wrap"><i class="fa {{ item.icon }}"></i></span> {{ item.title }}</div>
                <div class="body  {{ item.class }}">
                    {% for per in item.children %}
                        <a class="{{ per.class }}" href="{{ per.url }}">{{ per.title }}</a>
                    {% endfor %}
                </div>
            </div>
        {% endfor %}
    </div>
    

4. 路径导航

  • 权限初始化

    # 权限信息初始化(二级菜单)
        permission_queryset = curren_user_obj.roles.filter(permissions__isnull=False).values("permissions__id",
                                                                                             "permissions__title",
                                                                                             "permissions__url",
                                                                                             "permissions__pid_id",
                                                                                             "permissions__pid__title",
                                                                                             "permissions__pid__url",
                                                                                             "permissions__menu_id",
                                                                                             "permissions__menu__title",
                                                                                             "permissions__menu__icon").distinct()
    # 非菜单默认选中
        permission_list = []
        menu_dict = {}
        for item in permission_queryset:
            permission_list.append({"id": item["permissions__id"],
                                    "title": item['permissions__title'],
                                    "url": item['permissions__url'],
                                    "pid": item['permissions__pid_id'],
                                    "p_title": item['permissions__pid__title'],
                                    "p_url": item['permissions__pid__url']
                                    })
    
            menu_id = item["permissions__menu_id"]
            if not menu_id:
                continue
            node = {"id": item['permissions__id'], 'title': item['permissions__title'],
                    'url': item['permissions__url']}
            if menu_id in menu_dict:
                menu_dict[menu_id]['children'].append(node)
            else:
                menu_dict[menu_id] = {
                    'title': item['permissions__menu__title'],
                    'icon': item['permissions__menu__icon'],
                    'children': [node, ]
                }
        # 将登陆用户所拥有的权限存入session中,便于后续中间件中利用此数据进行权限校验
        request.session[settings.SESSION_PERMISSION_KEY] = permission_list
        request.session[settings.SESSION_MENU_KEY] = menu_dict
    
    
  • 中间件中生成重要导航条数据,并存储在request中,供后续调用

    class RbacMiddleware(MiddlewareMixin):
        def process_request(self, request):
            """
            当请求刚进入的时候被触发
            :param request:
            :return:
            """
            """
            1.获取当前用户请求的url
            2.获取当前用户在session中存储的权限列表 ['/customer/list/', '/customer/add/']
            3.权限进行匹配
            """
            valid_url_list = settings.VALID_URL_LIST
            # http://127.0.0.1:8000/customer/list/          -> /customer/list/
            # http://127.0.0.1:8000/customer/list/?age=18   -> /customer/list/
            current_url = request.path_info
            for valid_url in valid_url_list:
                if re.match(valid_url, current_url):
                    # 白名单中的url无需权限验证即可访问
                    return None
            permission_list = request.session.get(settings.SESSION_PERMISSION_KEY)
    
            if not permission_list:
                return HttpResponse("未获取到任何用户信息,请登陆!!!")
            flag = False
            url_record = [
                {"title": '首页', 'url': '#'}
            ]
            for item in permission_list:
                reg = "^%s$" % item['url']
                if re.match(reg, current_url):
                    flag = True
                    request.current_selected_permission = item['pid'] or item['id']
                    if not item['pid']:
                        url_record.extend([{'title': item['title'], 'url': item['url']}])
                    else:
                        url_record.extend([
                            {'title': item['p_title'], 'url': item['p_url']},
                            {'title': item['title'], 'url': item['url']},
                        ])
                    request.breadcrumb = url_record
                    break
            if not flag:
                return HttpResponse("无权访问")
    
  • html代码中循环导航条数据

            <div>
                <ol class="breadcrumb no-radius no-margin" style="border-bottom: 1px solid #ddd;">
                    {% for item in request.breadcrumb %}
                        <li>
                            <a href="{{ item.url }}">{{ item.title }}</a>
                        </li>
                    {% endfor %}
                </ol>
            </div>
    
posted @ 2025-07-31 11:45  幸福的好喜欢  阅读(34)  评论(0)    收藏  举报