Bootstrap

基于Django实现RBAC权限管理

概述

RBAC(Role-Based Access Control,基于角色的访问控制),通过角色绑定权限,然后给用户划分角色。在web应用中,可以将权限理解为url,一个权限对应一个url。

在实际应用中,url是依附在菜单下的,比如一个简单的生产企业管理系统,菜单可以大致分为以下几块:制造、资材、生产管理、人事、财务等等。每个菜单下又可以有子菜单,但最终都会指向一个url,点击这个url,通过Django路由系统执行一个视图函数,来完成某种操作。这里,制造部的员工登录系统后,肯定不能点击财务下的菜单,甚至都不会显示财务的菜单。

设计表关系

基于上述分析,在设计表关系时,起码要有4张表:用户,角色,权限,菜单:

  • 用户可以绑定多个角色,从而实现灵活的权限组合 :用户和角色,多对多关系
  • 每个角色下,绑定多个权限,一个权限也可以属于多个角色:角色和权限,多对多关系
  • 一个权限附属在一个菜单下,一个菜单下可以有多个权限:菜单和权限:多对一关系
  • 一个菜单下可能有多个子菜单,也可能有一个父菜单:菜单和菜单是自引用关系

其中角色和权限、用户和角色,是两个多对多关系,由Django自动生成另外两种关联表。因此一共会产生6张表,用来实现权限管理。

下面我们新建一个项目,并在项目下新建rbac应用,在该应用的models.py中来定义这几张表:

from django.db import models


class Menu(models.Model):
    """
    菜单
    """
    title = models.CharField(max_length=32, unique=True)
    parent = models.ForeignKey("Menu", null=True, blank=True) 
    # 定义菜单间的自引用关系
    # 权限url 在 菜单下;菜单可以有父级菜单;还要支持用户创建菜单,因此需要定义parent字段(parent_id)
    # blank=True 意味着在后台管理中填写可以为空,根菜单没有父级菜单

    def __str__(self):
        # 显示层级菜单
        title_list = [self.title]
        p = self.parent
        while p:
            title_list.insert(0, p.title)
            p = p.parent
        return '-'.join(title_list)


class Permission(models.Model):
    """
    权限
    """
    title = models.CharField(max_length=32, unique=True)
    url = models.CharField(max_length=128, unique=True)
    menu = models.ForeignKey("Menu", null=True, blank=True)

    def __str__(self):
        # 显示带菜单前缀的权限
        return '{menu}---{permission}'.format(menu=self.menu, permission=self.title)


class Role(models.Model):
    """
    角色:绑定权限
    """
    title = models.CharField(max_length=32, unique=True)

    permissions = models.ManyToManyField("Permission")
    # 定义角色和权限的多对多关系

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """
    用户:划分角色
    """
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    nickname = models.CharField(max_length=32)
    email = models.EmailField()

    roles = models.ManyToManyField("Role")
    # 定义用户和角色的多对多关系

    def __str__(self):
        return self.nickname

权限的初始化和验证

我们知道Http是无状态协议,那么服务端如何判断用户是否具有哪些权限呢?通过session会话管理,将请求之间需要”记住“的信息保存在session中。用户登录成功后,可以从数据库中取出该用户角色下对应的权限信息,并将这些信息写入session中。

所以每次用户的Http request过来后,服务端尝试从request.session中取出权限信息,如果为空,说明用户未登录,重定向至登录页面。否则说明已经登录(即权限信息已经写入request.session中),将用户请求的url与其权限信息进行匹配,匹配成功则允许访问,否则拦截请求。

我们先来实现第一步:提取用户权限信息,并写入session

为了实现rabc功能可在任意项目中的可用,我们单独创建一个rbac应用,以后其它项目需要权限管理时,直接拿到过,稍作配置即可。在rbac应用下新建一个文件夹service,写一个脚本init_permission.py用来执行初始化权限的操作:用户登录后,取出其权限及所属菜单信息,写入session中

from ..models import UserInfo, Menu


def init_permission(request, user_obj):
    """
    初始化用户权限, 写入session
    :param request: 
    :param user_obj: 
    :return: 
    """
    permission_item_list = user_obj.roles.values('permissions__url',
                                                 'permissions__title',
                                                 'permissions__menu_id').distinct()
    permission_url_list = []  
    # 用户权限url列表,--> 用于中间件验证用户权限
    permission_menu_list = []  
    # 用户权限url所属菜单列表 [{"title":xxx, "url":xxx, "menu_id": xxx},{},]

    for item in permission_item_list:
        permission_url_list.append(item['permissions__url'])
        if item['permissions__menu_id']:
            temp = {
  "title": item['permissions__title'],
                    "url": item["permissions__url"],
                    "menu_id": item["permissions__menu_id"]}
            permission_menu_list.append(temp)

    menu_list = list(Menu.objects.values('id', 'title', 'parent_id'))
    # 注:session在存储时,会先对数据进行序列化,因此对于Queryset对象写入session,加list()转为可序列化对象

    from django.conf import settings  
;