RBAC(Role-Based Access Control)是指基于角色的权限访问控制,是信息系统应用最广泛的权限控制方式。在RBAC中有3个要素:用户、角色、权限。
1.创建项目和应用
django-admin startproject rbac_template
cd rbac_template
python manage.py startapp rbac
在配置文件settings.py中注册应用
2.数据库表结构设计
需要创建5个数据库表:角色表、用户表、菜单表、权限表、权限组表
菜单表存放一级菜单;权限表主要存放权限信息,这个表中的数据记录分两种类型,一种存放纯权限信息的记录,另一种存放既是权限、又可以作为二级菜单的记录,我们约定这样的记录为“权限菜单”。权限组表实现对权限分组管理,一般把对同一个数据库表进行操作的权限分为一组。
各表之间的关系是:角色表与用户表是多对多关系,角色表与权限表是多对多关系,权限组表与权限表是一对多关系,菜单表与权限组表是一对多关系。
models.py:
from django.db import models
# 角色表:应用项目可以根据需求在这个表中增加角色记录
class Role(models.Model):
"""角色名,字段类型为CharField,unique=True设置角色名不能重复
verbose_name="角色名"设置在Django Admin管理后台字段名为角色名
如果不设置verbose_name,则管理后台中显示字段名为title"""
title = models.CharField(max_length=32, unique=True, verbose_name="角色名")
# 定义角色和权限的多对多关系,也就是一个角色可以有多个权限
# 一个权限也可以被多个角色拥有
permissions = models.ManyToManyField("Permission", blank=True, verbose_name="拥有权限")
# 定义数据模型实例对象名称
def __str__(self):
return self.title
# 定义数据库表在管理后台的表名
class Meta:
verbose_name_plural = "角色表"
# 用户表:应用项目的用户要放在这个表中
class UserInfo(models.Model):
# 登录账号
username = models.CharField(max_length=32)
# 用户密码
password = models.CharField(max_length=64)
# 用户姓名
nickname = models.CharField(max_length=32)
# 用户邮箱,定义为Email Field类型,保存时会校验格式是否符合邮箱的格式
email = models.EmailField()
# 定义用户和角色的多对多关系
roles = models.ManyToManyField("Role")
def __str__(self):
return self.nickname
class Meta:
verbose_name_plural = "用户表"
# 权限表,用户根据应用项目划分好权限,然后输入这张表
class Permission(models.Model):
# 权限名称title,通过unique=True设置名称不能重复
title = models.CharField(max_length=32, unique=True, verbose_name="权限名称")
# url字段存放URL正则表达式,用来与URL配置项相对应
url = models.CharField(max_length=128, unique=True, verbose_name="URL")
# 权限代码字段perm_code,起到标识权限的作用,相当于权限的别名
# 一般是list、add、del、edit
perm_code = models.CharField(max_length=32, verbose_name="权限代码")
"""
权限分组字段,主要作用为把一类权限分在一组中
通过外键形式与PermGroup建立多对一的关系,一个权限组下有多个权限
通过设置on_delete=models.CASCADE(Django规定外键的属性必须有on_delete设置)
models.CASCADE起到的作用为
当外键关联的PermGroup中的记录被删除时,本表中的相关联的记录也将被删除
"""
perm_group = models.ForeignKey(to='PermGroup', blank=True, on_delete=models.CASCADE, verbose_name="所属权限组")
"""
这个外键与本表中记录进行关联,可称作内联外键
也就是pid字段与本表中的id字段形成多对一的关系,id是Django在建数据库表时生成的主键
当pid字段值为空时,约定为二级菜单,这条记录就是前面介绍的“权限菜单
"""
pid = models.ForeignKey(to='Permission', null=True, blank=True, on_delete=models.CASCADE, verbose_name="所属二级菜单")
def __str__(self):
# 显示带菜单前缀的权限
return self.title
class Meta:
verbose_name_plural = "权限表"
class PermGroup(models.Model):
# 权限组名
title = models.CharField(max_length=32, verbose_name="组名称")
# 外键,与Menu表是多对一的关系,一个一级菜单可以有一个或多个权限组
menu = models.ForeignKey(to="Menu", verbose_name="所属菜单", blank=True, on_delete=models.CASCADE)
def __str__(self):
return self.title
class Meta:
verbose_name_plural = "权限组"
# 菜单表,可以根据应用系统所拥有的菜单,输入这个表中
class Menu(models.Model):
# 菜单名称
title = models.CharField(max_length=32, unique=True, verbose_name="一级菜单")
def __str__(self):
return self.title
class Meta:
verbose_name_plural = "一级菜单表"
配置setting.py:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
执行命令生成表
# 校验数据模型代码正确性,生成操作数据库的SQL语句、相关日志
python manage.py makemigrations
# 生成数据库表
python manage.py migrate
3.用户权限数据初始化配置
在rbac目录中创建一个server文件夹,在其下创建一个init_permission.py文件。这个文件主要有两个功能。一个功能是根据用户所属角色从数据库表中获取此用户的权限,然后按一定的数据格式存放在request.session中;另一个功能是把该用户涉及的菜单、权限组、权限等信息按数据表关联关系取出,按一定的数据格式也存放在request.session中。
# 导入配置文件settings.py
from django.conf import settings
"""
这个函数一般在用户登录后接着调用,根据用户权限进行数据初始化
初始化用户权限,写入session中
参数request是Request请求对象
参数user_obj是登录用户对象,取自UserInfo表
"""
def init_permission(request, user_obj):
"""
以下代码按照Django ORM查询语句语法取值
首先通过user_obj.roles取得用户对象具有的所有角色对象
roles是UserInfo表中的字段,是多对多键,通过它关联到Role表
values()通过外键字段加双下划线的方式取得关联表中的字段值
例如,permissions__id取得Permission表中的id字段值
permissions是role的字段,
是个外键字段,关联Permission表
permissions__pid_id中pid_id指的是Permission表的pid_id字段,对应的是数据模型的pid
因为数据模型生成数据库表时,凡是外键字段会在数据库中字段名后加'_id'
同理permissions__perm_group_id指的是Permission表的perm_group_id字段
(这个字段是外键字段,对应的是数据模型中的perm_group)
permissions__perm_group__menu_id先通过Role表中permissions字段关联到Permission表
再通过Permission表中的perm_group字段关联到PermGroup表
最后取得PermGroup表中的menu_id字段(这个字段是外键字段,对应数据模型中的menu)
同理permissions__perm_group__menu__title取得Menu表中的title字段
最后用distinct()删去重复的记录
"""
permission_item_list = user_obj.roles.values('permissions__id', 'permissions__title', 'permissions__url',
'permissions__perm_code', 'permissions__pid_id',
'permissions__perm_group_id', 'permissions__perm_group__menu_id',
'permissions__perm_group__menu__title', ).distinct()
print('permission_item_list:', permission_item_list)
# 初始化一个空字典变量
permission_url_dict = {}
# 初始化一个列表变量
permission_menu_list = []
"""
通过循环取出permission_item_list中的值以填充permission_url_list字典
字典以perm_group_id为分组标准,取得用户权限中的url,code
该字典的具体结构:以权限组的id(perm_group_id)为键名
键值由二级字典组成,二级字典有两个键值对,一个键名为codes,其键值为列表,列表项为权限代码(perm_code),
另一个键名为urls,键值为列表,列表项为URL(url)
"""
for item in permission_item_list:
perm_group_id = item['permissions__perm_group_id']
url = item['permissions__url']
perm_code = item['permissions__perm_code']
if perm_group_id in permission_url_dict:
permission_url_dict[perm_group_id]['codes'].append(perm_code)
permission_url_dict[perm_group_id]['urls'].append(url)
else:
permission_url_dict[perm_group_id] = {'codes': [perm_code, ], 'urls': [url, ]}
print("permission_url_dict:", permission_url_dict)
# 把permission_url_dict存在session中
# session中的键名用的是settings.py文件中PERMISSION_URL_KEY变量的值
# 前提是settings.py文件设有这个变量
request.session[settings.PERMISSION_URL_KEY] = permission_url_dict
"""
通过循环取出permission_item_list中的值以填充permission_menu_list列表
列表以权限id为分组标准,取得与菜单名、权限名、URL相关的值
该列表的具体结构:列表每项都是字典类型
主要有权限id、名称、url、pid、一级菜单id和名称等值
"""
for item in permission_item_list:
# 形成一个字典tpl
tpl = {'id': item['permissions__id'], 'title': item['permissions__title'], 'url': item['permissions__url'],
'pid_id': item['permissions__pid_id'], 'menu_id': item['permissions__perm_group__menu_id'],
'menu_title': item['permissions__perm_group__menu__title']}
# 把字典加入列表中
permission_menu_list.append(tpl)
print("permission_menu_list:", permission_menu_list)
request.session[settings.PERMISSION_MENU_KEY] = permission_menu_list
4.利用中间件验证用户权限
Django中间件中的方法在客户端发出请求后、调用视图函数前或服务器发出响应后、到达客户端前等阶段运行。其中process_request( )方法在浏览器发出请求后、调用视图函数前运行。
在rbac目录中创建一个middleware文件夹,在其下创建一个rbac.py文件。在这个文件编写Rbac Middleware类,这个类继承于Middleware Mixin,因此是一个中间件类。中间件类要让Django知道并调用,必须要在配置文件settings.py的MIDDLEWARE代码块中注册,代码如下,其中'rbac.middleware.rbac.Rbac Middleware'这一行代码注册了这个中间件。
from django.conf import settings
from django.shortcuts import HttpResponse, redirect
from django.utils.deprecation import MiddlewareMixin
# 导入正则表达式模块
import re
# 定义的中间件都继承于MiddlewareMixin
class RbacMiddleware(MiddlewareMixin):
"""
process_request()方法,是中间件原有的方法
这个方法在客户端发出request请求后、执行视图函数前调用
任何request请求都会先调用process_request()方法
该方法无返回、返回None或HttpResponse 对象时,程序将继续执行其他中间件
直到执行相应的视图
如果它返回一个 HttpResponse对象,程序中断执行,向客户端返回HttpResponse
我们重写这个方法,主要判断登录用户对当前要访问的URL是否有权限
如果有权限则返回None,程序继续向下执行,无权限则返回HttpResponse对象中止程序向下运行
"""
def process_request(self, request):
# 从请求中取得URL,这个地址是用户请求地址
# request.path_info得到请求的路径
# 如http://127.0.0.1:8000/index/的path_info是/index/
request_url = request.path_info
# 从sessoin中取出init_permission中生成的字典,这个字典包含用户可以访问的URL
permission_url = request.session.get(settings.PERMISSION_URL_KEY)
# print('访问url', request_url)
# print('权限--', permission_url)
# 在settings.py文件中,SAFE_URL保存无须权限、直接访问的URL,称为URL白名单
# 如果请求URL在白名单,直接return None放行
for url in settings.SAFE_URL:
if re.match(url, request_url):
return None
# 如果是超级用户,不进行权限审查
if request.user.is_superuser:
return None
# 如果未取得permission_url,说明用户没登录,重定向到登录页面
if not permission_url:
return redirect(settings.LOGIN_URL)
flag = False
"""
通过for perm_group_id,code_url in permission_url.items()循环
取出一级字典的键与值
通过for url in code_url['urls']循环取URL
用这个URL生成正则表达式(url_pattern ="^{0}$".format(url))
如果用户请求访问的地址与这个表达式匹配,说明用户有权限
"""
for perm_group_id, code_url in permission_url.items():
for url in code_url['urls']:
url_pattern = "^{0}$".format(url)
# print(url_pattern)
if re.match(url_pattern, request_url):
# 把权限代码放在session中
request.session['permission_codes'] = code_url["codes"]
flag = True
break
if flag:
return None
if not flag:
# 如果是调试模式,显示可访问URL
if settings.DEBUG:
info = '<br/>' + ('<br/>'.join(code_url['urls']))
return HttpResponse('无权限,请尝试访问以下地址:%s' % info)
else:
return HttpResponse('无权限访问')
5.生成系统菜单所需数据
在rbac目录中创建一个templatetags文件夹,再在它的下面创建一个custom_tag.py文件,代码如下
from django import template
from django.conf import settings
import re, os
# 导入mark_safe()函数
from django.utils.safestring import mark_safe
# 生成一个模板类库
register = template.Library()
"""
处理init_permission生成的数据结构,生成系统菜单的所需的数据结构
这个函数共有3个循环
第一个循环形成二级菜单字典
第二个循环把当前二级菜单设置为'active': True
第三个循环把一级菜单、二级菜单放在一个数据结构中,并分清层次
"""
def get_structure_data(request):
# 取出当前请求的URL
current_url = request.path_info
# 取出init_permission生成的数据,这是一个列表类型,每个列表项是字典类型
perm_menu = request.session[settings.PERMISSION_MENU_KEY]
print("perm_menu:", perm_menu)
# 初始化一个空字典
menu_dict = {}
"""
以下for循环的目的是
获得权限菜单(二级菜单),判断依据,pid_id为空即为二级菜单
通过循环取出perm_menu的每一个列表项(字典类型),判断字典中的pid_id是否为空
为空时,把该列表项(字典类型)加入menu_dict字典
键名是列表项字典中的id,键值是该列表项
这样menu_dict形成一个包含两级的字典
"""
for item in perm_menu:
# not item["pid_id"]成立说明pid_id为空
if not item["pid_id"]:
menu_dict[item["id"]] = item.copy()
print("menu_dict:", menu_dict)
"""
以下for循环目的是
在menu_dict字典中
给当前(用户选中的)二级菜单所在的二级字典加一个键值对'active': True
判断依据为二级菜单记录的URL与用户请求地址匹配
通过循环取出perm_menu的每一个列表项(字典类型),用字典中的url值生成正则表达式
如果用户请求的URL与这个表达式匹配,再判断列表项字典中pid_id的值是否为空
如果为空,到menu_dict的二级字典中加一个键值对'active':True
当pid_id不为空时,找到menu_dict中id值等于pid_id值的二级字典加一个键值对'active': True
这个流程就是:如果二级菜单被应用,这个二级菜单就被激活('active': True)
如果隶属于二级菜单的权限被选中,这个二级菜单也被设为激活状态('active': True)
"""
for item in perm_menu:
regex = "^{0}$".format(item["url"])
if re.match(regex, current_url):
# print(current_url)
if not item["pid_id"]:
menu_dict[item["id"]]["active"] = True
else:
# 非权限菜单记录,把本记录隶属的二级菜单的active设置为True
menu_dict[item["pid_id"]]["active"] = True
print("menu_dict:", menu_dict)
menu_result = {}
"""以下for循环代码块目的是
把一级菜单与二级菜单放在一起,并分出层次
通过循环menu_dict字典的键值项,获取每项键值(二级字典)
取出字典的menu_id、active值
开始给menu_result赋值,这个字典是多层的,
一级是字典,键名是menu_id的值,键值是字典类型,包含了一级菜单的相关内容和一个children键
这个键值是一个字典类型,可以算是二级字典
在二级字典中,children键值是一个列表,在列表中以字典形式加入二级菜单的信息
也就是每一个列表项是一个字典,这个二级菜单隶属于一级菜单
如果二级菜单的active等于True,那么它隶属的一级菜单也设为'active': True
其他一级菜单二级菜单都设为'active': None
"""
for item in menu_dict.values():
# 给active变量赋值,如果取不到值,则active=None
active = item.get("active")
menu_id = item.get("menu_id")
if menu_id in menu_result:
# 如果menu_id已存在menu_result字典中
# 则为二级字典中的children键值增加一个项(该项是字典类型)
menu_result[menu_id]["children"].append({'title': item['title'], 'url': item['url'], 'active': active})
if active:
# 设置一级菜单的active为True
menu_result[menu_id]["active"] = True
else:
"""
如果menu_id在menu_result字典中不存在,先生成一级字典
键名为一级菜单的id(menu_id),一级字典的键值也是字典类型
这算是二级字典,二级字典存放的是一级菜单信息和children键
在二级字典中children的键值又是一个列表,在列表加入二级菜单信息
这些信息以字典的形式存放,也就是每一个列表项是一个字典类型
"""
menu_result[menu_id] = {'menu_id': menu_id, 'menu_title': item['menu_title'], 'active': active,
'children': [{'title': item['title'], 'url': item['url'], 'active': active}]}
print("menu_result:", menu_result)
# 返回生成的数据结构
return menu_result
# inclustion_tag对一段HTML代码进行渲染改造后并返回
@register.inclusion_tag("fare/rbac_menu.html")
def rbac_menu(request):
menu_data = get_structure_data(request)
print("menu_data:", menu_data)
return {'menu_result': menu_data}
rbac_menu.html根据传入的数据进行菜单展示。