Django 基于RBAC的通用权限管理实现

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根据传入的数据进行菜单展示。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RBAC(Role-Based Access Control)是一种基于角色的访问控制,它将权限授予角色,然后将角色授予用户。在Django中,可以使用django-guardian或django-rules等第三方库来实现RBAC权限管理,也可以自己编写代码实现。 下面是一个基于Django自己编写的简单RBAC权限管理系统的实现过程: 1. 定义权限模型 在Django中,可以通过定义模型来表示权限。例如,可以定义一个Permission模型,用来表示系统中的所有权限: ``` from django.db import models class Permission(models.Model): name = models.CharField(max_length=255, unique=True) codename = models.CharField(max_length=100, unique=True) ``` 其中name字段表示权限的名称,codename字段表示权限的代码名称。 2. 定义角色模型 同样地,可以定义一个Role模型,用来表示系统中的所有角色: ``` from django.db import models class Role(models.Model): name = models.CharField(max_length=255, unique=True) permissions = models.ManyToManyField('Permission') ``` 其中name字段表示角色的名称,permissions字段表示角色拥有的权限。 3. 定义用户模型 可以使用Django自带的User模型或者自己定义一个用户模型。在用户模型中,可以添加一个roles字段,用来表示用户所拥有的角色: ``` from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): roles = models.ManyToManyField('Role') ``` 4. 编写权限检查装饰器 在Django中,可以使用装饰器来检查用户是否拥有某个权限。下面是一个简单的权限检查装饰器的实现: ``` from functools import wraps from django.http import HttpResponseForbidden def permission_required(perm): def decorator(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.has_perm(perm): return HttpResponseForbidden() return view_func(request, *args, **kwargs) return _wrapped_view return decorator ``` 在上面的代码中,permission_required装饰器接受一个权限codename作为参数,返回一个装饰器函数。这个装饰器函数接受一个视图函数作为参数,返回一个新的视图函数。新的视图函数在执行前会检查用户是否拥有该权限,如果没有则返回403禁止访问状态码。 5. 在视图函数中使用权限检查装饰器 在需要进行权限检查的视图函数上加上permission_required装饰器即可: ``` @permission_required('app.view_model') def my_view(request): # do something ``` 在上面的代码中,my_view函数需要拥有view_model权限才能访问。 6. 编写角色管理视图 可以编写一个简单的角色管理视图,用来管理系统中的角色和权限: ``` from django.shortcuts import render from .models import Role, Permission def role_list(request): roles = Role.objects.all() return render(request, 'role_list.html', {'roles': roles}) def role_detail(request, role_id): role = Role.objects.get(id=role_id) permissions = Permission.objects.all() return render(request, 'role_detail.html', {'role': role, 'permissions': permissions}) ``` 在上面的代码中,role_list函数返回所有角色的列表,role_detail函数返回指定角色的详细信息和所有权限的列表。 7. 编写用户角色管理视图 可以编写一个简单的用户角色管理视图,用来管理用户和角色的关联关系: ``` from django.shortcuts import render from django.contrib.auth.models import User from .models import Role def user_list(request): users = User.objects.all() return render(request, 'user_list.html', {'users': users}) def user_detail(request, user_id): user = User.objects.get(id=user_id) roles = Role.objects.all() return render(request, 'user_detail.html', {'user': user, 'roles': roles}) ``` 在上面的代码中,user_list函数返回所有用户的列表,user_detail函数返回指定用户的详细信息和所有角色的列表。 8. 编写用户角色管理视图的表单处理函数 可以编写一个简单的表单处理函数,用来处理用户和角色的关联关系: ``` from django.shortcuts import redirect from django.contrib.auth.models import User from .models import Role def assign_role(request, user_id): if request.method == 'POST': user = User.objects.get(id=user_id) role_ids = request.POST.getlist('roles') roles = Role.objects.filter(id__in=role_ids) user.roles.set(roles) return redirect('user_detail', user_id=user_id) ``` 在上面的代码中,assign_role函数接受一个用户ID作为参数,从POST请求中获取选中的角色ID,将这些角色和用户关联起来,然后重定向到用户详细信息页面。 9. 编写角色管理视图的表单处理函数 可以编写一个简单的表单处理函数,用来处理角色和权限的关联关系: ``` from django.shortcuts import redirect from .models import Role, Permission def assign_permission(request, role_id): if request.method == 'POST': role = Role.objects.get(id=role_id) permission_ids = request.POST.getlist('permissions') permissions = Permission.objects.filter(id__in=permission_ids) role.permissions.set(permissions) return redirect('role_detail', role_id=role_id) ``` 在上面的代码中,assign_permission函数接受一个角色ID作为参数,从POST请求中获取选中的权限ID,将这些权限和角色关联起来,然后重定向到角色详细信息页面。 10. 编写模板 最后,可以编写一些简单的模板来渲染上述视图函数返回的数据。 例如,可以编写role_list.html模板来渲染角色列表: ``` <ul> {% for role in roles %} <li><a href="{% url 'role_detail' role.id %}">{{ role.name }}</a></li> {% endfor %} </ul> ``` 可以编写role_detail.html模板来渲染角色详细信息和权限列表: ``` <h1>{{ role.name }}</h1> <h2>Permissions</h2> <form method="post" action="{% url 'assign_permission' role.id %}"> {% csrf_token %} {% for permission in permissions %} <label> <input type="checkbox" name="permissions" value="{{ permission.id }}" {% if permission in role.permissions.all %}checked{% endif %}> {{ permission.name }} </label> {% endfor %} <button type="submit">Save</button> </form> ``` 可以编写user_list.html模板来渲染用户列表: ``` <ul> {% for user in users %} <li><a href="{% url 'user_detail' user.id %}">{{ user.username }}</a></li> {% endfor %} </ul> ``` 可以编写user_detail.html模板来渲染用户详细信息和角色列表: ``` <h1>{{ user.username }}</h1> <h2>Roles</h2> <form method="post" action="{% url 'assign_role' user.id %}"> {% csrf_token %} {% for role in roles %} <label> <input type="checkbox" name="roles" value="{{ role.id }}" {% if role in user.roles.all %}checked{% endif %}> {{ role.name }} </label> {% endfor %} <button type="submit">Save</button> </form> ``` 至此,一个简单的RBAC权限管理系统就完成了。当然,实际应用中可能需要更复杂的权限管理需求,可以根据具体情况进行扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值