组件实现效果图
模型model
业务场景:不同用户拥有不同的权限,权限的本质则=能访问URL的数量。用户级别越高,权限越高,能访问的URL也越多
表设计:用户表、角色表、权限表、菜单表
用户表
元数据Meta:abstract。让实际业务的用户表继承RBAC的用户表
用户表字段可自行根据业务场景进行修改,但必须保留roles字段多对多角色表
角色表
字段内容不变,需要与权限表进行多对多permissions
权限表
字段内容不变,需要与菜单menu进行多对一
pid与自身进行ForeignKey,to=Permission
或to='self'
一样的效果,有pid的为三级权限,即不能成为菜单的权限
菜单表
能够成为一级菜单的权限
from django.db import models
class Menu(models.Model):
"""
菜单
"""
title = models.CharField(verbose_name='菜单', max_length=32)
icon = models.CharField(verbose_name='图标', max_length=32)
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, unique=True)
name = models.CharField(verbose_name='代码', max_length=64, unique=True, null=False, blank=False)
# 让不能作为菜单的权限,关联能成为菜单的权限。为了进入非菜单URL后,左侧菜单栏自动展开和此URL关联的权限菜单
pid = models.ForeignKey(verbose_name='默认选中权限', to='Permission', related_name='ps', null=True, blank=True,
help_text="对于无法作为菜单的URL,可以为其选择一个可以作为菜单的权限,那么访问时,则默认选中此权限",
limit_choices_to={'menu__isnull': False}, on_delete=models.SET_NULL)
menu = models.ForeignKey(verbose_name='菜单', to='Menu', null=True, blank=True, help_text='null表示非菜单',
on_delete=models.SET_NULL) # 如果有meue值就为1级菜单,没有则为2级菜单
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):
"""
用户表
"""
username = models.CharField(verbose_name='用户名', max_length=32,null=True,blank=True)
password = models.CharField(verbose_name='密码', max_length=64,null=True,blank=True)
email = models.CharField(verbose_name='邮箱', max_length=32,null=True,blank=True)
roles = models.ManyToManyField(verbose_name='拥有的所有角色', to=Role, blank=True)
class Meta:
abstract = True
编写业务的用户登陆,并初始化用户权限
在主业务中编写用户登录视图,登陆成功后进行权限的初始化
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
user = request.POST.get('username')
pwd = request.POST.get('password')
user_object = models.UserInfo.objects.filter(name=user, password=pwd).first()
if not user_object:
return render(request, 'login.html', {'error': '用户名或密码错误'})
# 用户权限信息的初始化
init_permission(user_object, request)
return redirect('/index/')
在用户登陆后,需要进行初始化该用户的权限
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from django.conf import settings
def init_permission(current_user, request):
"""
用户权限的初始化
:param current_user: 当前用户对象
:param request: 请求相关所有数据
:return:
"""
# 2. 权限信息初始化
# 根据当前用户信息获取此用户所拥有的所有权限,并放入session。
# 根据角色获取所有权限
# permissions__id__isnull=False为了防止新增加了职位,但是没有数据为null,需要对该数据进行排除
# distinct()防止同一个用户有多个职位,多个职位有相同的权限,而出现重复数据,需要进行去重处理
permission_queryset = current_user.roles.filter(permissions__isnull=False).values("permissions__id",
"permissions__title",
"permissions__url",
"permissions__name",
"permissions__pid_id",
"permissions__pid__title",
"permissions__pid__url",
"permissions__menu_id",
"permissions__menu__title",
"permissions__menu__icon"
).distinct()
# 3. 获取权限+菜单信息
permission_dict = {}
menu_dict = {}
for item in permission_queryset:
###### 处理权限 ######
permission_dict[item['permissions__name']] = {
'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'] # 拿到menu_id用于确认1级菜单
if not menu_id: # 没有menu_id(null)
continue
# menu_id有值,则该权限为menu_id的1级菜单所属的2级菜单
node = {'id': item['permissions__id'], # 拿到有menu_id的,权限id,即二级菜单的id
'title': item['permissions__title'],
'url': item['permissions__url']}
if menu_id in menu_dict: # 该menu_id已经为1级菜单,则为该1级菜单添加2级菜单
menu_dict[menu_id]['children'].append(node) # 在1级菜单下添加2级菜单
else: # 添加新的1级菜单
menu_dict[menu_id] = {
'title': item['permissions__menu__title'],
'icon': item['permissions__menu__icon'],
'children': [node, ] # 添加2级菜单
}
# 将权限信息和菜单信息 放入session
request.session[settings.PERMISSION_SESSION_KEY] = permission_dict
request.session[settings.MENU_SESSION_KEY] = menu_dict
获得该用户对应的菜单和权限组成字典
菜单menu_dict
{
"1": {
"title": "客户管理",
"icon": "fa-address-book-o",
"children": [
{
"id": 22,
"title": "客户列表",
"url": "/stark/crm/customer/list/"
},
{
"id": 26,
"title": "公户",
"url": "/stark/crm/customer/pub/list/"
},
{
"id": 30,
"title": "私户",
"url": "/stark/crm/customer/pri/list/"
}
]
},
"2": {
"title": "教学管理",
"icon": "fa-bar-chart-o",
"children": [
{
"id": 10,
"title": "课程列表",
"url": "/stark/crm/course/list/"
},
{
"id": 18,
"title": "班级列表",
"url": "/stark/crm/classlist/list/"
},
{
"id": 42,
"title": "学生列表",
"url": "/stark/crm/student/list/"
},
{
"id": 46,
"title": "上课记录",
"url": "/stark/crm/courserecord/list/"
}
]
},
........
}
权限permission_dcit
{
'stark:crm_department_changelist': {
'id': 1,
'title': '部门列表',
'url': '/stark/crm/department/list/',
'pid': None,
'pid_url': None,
'pid_name': None
},
'stark:crm_department_add': {
'id': 2,
'title': '添加部门',
'url': '/stark/crm/department/add/',
'pid': 1,
'pid_url': '/stark/crm/department/list/',
'pid_name': 'stark:crm_department_changelist'
},
'stark:crm_department_change': {
'id': 3,
'title': '修改部门',
'url': '/stark/crm/department/(?P<pk>\\d+)/change/',
'pid': 1,
'pid_url': '/stark/crm/department/list/',
'pid_name': 'stark:crm_department_changelist'
},
'stark:crm_department_del': {
'id': 4,
'title': '删除部门',
'url': '/stark/crm/department/(?P<pk>\\d+)/del/',
'pid': 1,
'pid_url': '/stark/crm/department/list/',
'pid_name': 'stark:crm_department_changelist'
},
'stark:crm_userinfo_changelist': {
'id': 5,
'title': '用户列表',
'url': '/stark/crm/userinfo/list/',
'pid': None,
'pid_url': None,
'pid_name': None
},
.......
}
在settings.py中配置初始化用户权限session的key
# 自定制key
MENU_SESSION_KEY = "u8fkksjdfkjsf"
PERMISSION_SESSION_KEY = "u8fkkasssjdfkjsfffff"
在中间键中,过滤出登陆用户能够访问的URL,即该用户拥有的权限
在rbac这个app内创建middleware/rbac.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse, reverse, redirect
from django.conf import settings
class RbacMiddleware(MiddlewareMixin):
"""
用户权限信息校验
"""
def process_request(self, request):
"""
当用户请求刚进入时候出发执行
:param request:
:return:
"""
"""
1. 获取当前用户请求的URL
2. 获取当前用户在session中保存的权限列表 ['/customer/list/','/customer/list/(?P<cid>\\d+)/']
3. 权限信息匹配(匹配路径这种有正则符号的字符串不能使用==,需要使用re.match(样本,比较的素材)进行正则匹配)
"""
# 1.白名单中的URL无需权限验证即可访问
current_url = request.path_info # 当前访问的URL
for valid_url in settings.VALID_URL_LIST:
if re.match(valid_url, current_url):
return None
# 2.获取当前用户权限列表
permission_dict = request.session.get(settings.PERMISSION_SESSION_KEY)
if not permission_dict: # 如果session中没有该列表,则需要进行登陆
return HttpResponse('未获取到用户权限信息,请登录!')
# return redirect(reverse('login')) 可自行根据name进行反向生成
# 3.定义路径导航格式
url_record = [
{'title': '首页', 'url': '#'}
]
# 4.需要登录,但无需权限校验的URL
for url in settings.NO_PERMISSION_LIST:
if re.match(url, current_url):
request.current_selected_permission = 0 # 给request注册一个全局属性:当前选中的URL,0为没有选中任何菜单URL
request.breadcrumb = url_record # 注册全局属性breadcrumb用来更方便获取路径导航数据url_record
request.menu_name = '首页' # 当前路径导航名称
return None
flag = False
for item in permission_dict.values(): # 获取每个权限name对应的values
reg = "^%s$" % item['url'] # 匹配URL的完整路径,防止出现多个匹配
if re.match(reg, current_url):
flag = True
request.current_selected_permission = item['pid'] or item['id'] # 当前菜单选中的URL的id,如果有pid则为pid,没有pid则为id
if not item['pid']: # 没有pid,则为二级菜单
# extend列表,拼接该权限的路径导航,class:active选中该路径
url_record.extend([{'title': item['title'], 'url': item['url'], 'class': 'active'}])
else: # 有pid则为pid二级菜单所属的普通权限
# 有pid,则拼接路径,三级路径需要在二级菜单的后面,并选中该权限active
url_record.extend([
{'title': item['p_title'], 'url': item['p_url']}, # 该权限对应的二级菜单
{'title': item['title'], 'url': item['url'], 'class': 'active'},
])
request.breadcrumb = url_record
break
# 获取路径导航对应的目录名
for url in url_record:
if "class" in url:
request.menu_name = url.get("title")
if not flag: # 没有匹配的权限,则无权访问
return HttpResponse('无权访问') # 可进行自定制,例如返回404页面
request.breadcrumb
为页面生成路径导航,格式:
url_record [
{'title': '首页', 'url': '#'},
{'title': '权限分配', 'url': '/rbac/distribute/permissions/', 'class': 'active'}
]
request.current_selected_permission
用于左侧菜单判断,菜单ID对应当前的访问的菜单ID,则对应的菜单会展开
页面以layout_plus.html为基础,所有页面继承该页面
layout_plus.html开放了两个templatetags接口,分别为左侧菜单栏和路径导航,更方便进行自定制