关于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.pydef 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.pyimport 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 -
获取菜单信息并保存到sessiondef 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获取菜单信息并保存到sessionfrom 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_dicthtml模板中显示菜单信息(从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>




浙公网安备 33010602011771号