Django - 权限系统设计与实现

背景

权限系统在后台中不可避免,本文分享一下咱们的权限系统实现方案。python

在分享前先简单介绍一下咱们的平台业务。咱们是质量部,咱们的平台对接了多个业务部门,所以须要实现:git

  • 多用户
  • 多项目
  • 3 种角色

不一样用户在不一样部门的项目中拥有一种角色,每种角色对不一样的接口有不一样的操做权限,例如:github

  • 只有 Admin 可以删除数据
  • 全部用户都有数据查看权限
  • 只有 Operator 可以修改数据

以上就是简化后的权限系统的需求,下面讲讲实现方案。数据库

设计与实现

Django - 模型序列化返回天然主键值 一文中咱们了解过 DRF 的序列化模块,除了序列化,DRF 还封装好了不少好用的功能,好比咱们目前平台的 APIView 就是继承自 DRF 的 APIView 类,还有分页类(Pagination)和权限控制类(Permission)等等。django

咱们实现权限控制的方案就借鉴了 DRF 的 DjangoModelPermission 类。bash

Django 的权限模块其实已经有 UserGroupPermission 数据模型以及关联关系,之因此不用官方的权限也不直接用 DRF 的权限模块是由于这二者都基于数据模型的 CURD 作判断,可配置但配置与数据迁移相对麻烦,重点是业务不须要精细与灵活的权限配置,所以没有采用。session

角色关系

用户在不一样项目中拥有不一样角色,同时一个项目也会有多个用户,所以用户、项目与角色的关系为:由用户与项目组成组合主键,对应一个角色。app

用户-项目-角色关系:post

项目-用户-角色关系:单元测试

一张表能够输出一个用户在不一样产品中的角色,以及一个产品中的全部用户与对应的权限两个维度信息,方便从两种维度对角色进行配置。

数据模型

from django.db import models
from django.conf import settings
from myapp.codes import role

class UserProjectRole(models.Model):
    """用户-项目-角色关系表"""
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    project = models.ForeignKey('myapp.Project', on_delete=models.CASCADE)
    role = models.IntegerField(default=role.GUEST)

    class Meta:
        db_table = 'myapp_user_project_role'
        unique_together = ['user', 'project']
复制代码

角色与 Session

虽然一个用户在不一样的项目拥有不一样的角色,可是用户同时只能访问一个项目,因此能够直接将当前产品以及对应的角色直接存于该用户的 Session 中,减小频繁查询数据库的过程。

代码实现

def get_or_create_role(user_id, project_id, session, default_role=role.GUEST):
    """根据session获取当前用户的角色"""
    role = session.get('role')
    if not role:
        role_rel_obj, _ = AuthGroup.objects.get_or_create(
                              user_id=user_id,
                              project_id=project_id,
                              default={'role': default_role})
        session['role'] = role_rel_obj.role
        
    return role
复制代码

角色初始化

在用户首次选择某项目时,向 myapp_user_project_role 表中插入一条数据。

值得注意的是,Django 的 auth_user 表中有现成的字段能够用于判断用户是否为管理员。我以 auth_user.is_staff == 1 为管理员,管理员权限只可经过 Django 的 admin 站点进行修改,确保管理员用户不会被随便升级降级。

用户如果管理员,则插入 admin 角色;不然插入 guest 角色。Operator 角色经过配置接口进行建立。

代码实现

class SelectProject(MyAPIView):
    def post(self, request, project_id):
        """选择项目"""
        default_role = role.ADMIN if is_admin(request.user) else role.GUEST
        role_rel_obj, _ = UserProjectRole.objects.get_or_create(
                              user=request.user,
                              project_id=project_id,
                              defaults={'role': default_role})
        request.session['role'] = role_rel_obj.role
        ...
复制代码

权限关系

权限主要指对各接口发送到不一样请求方法的操做权限。

接口-请求方法-角色关系:

DRF 的 DjangoModelPermission 类

DjangoModelPermission 完整源码可访问其 源码

如今咱们分析一下这个类的实现。

首先是 docstring 中的描述:It ensures that the user is authenticated, and has the appropriate add/change/delete permissions on the model.,以及一个请求类型与权限的映射关系结构:

perms_map = {
    'GET': [],
    'OPTIONS': [],
    'HEAD': [],
    'POST': ['%(app_label)s.add_%(model_name)s'],
    'PUT': ['%(app_label)s.change_%(model_name)s'],
    'PATCH': ['%(app_label)s.change_%(model_name)s'],
    'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
复制代码

能够看到,这个权限类是根据数据模型的 CURD 与请求类型的关系进行权限控制。

而后看看两个类方法的定义:

  • get_required_permissions:给出一个请求类型,返回该请求类型须要的权限列表
  • has_permission:判断用户是否有权限执行本次请求

has_permission 方法在父类 BasePermission 中定义,返回 True 则表示有权限,不然会在 APIView 中被捕获,返回 403

有了大概的逻辑,咱们就能重写一个 RolePermissions 类。

代码实现

咱们既然直接针对接口的不一样请求方法作控制,那么咱们就须要定义每一个请求方法对应的权限列表。为简化写法,我把列表改成最小须要的权限:

class MyAPI(MyAPIView):
    min_perms_map = {
        'POST': role.OPERATOR,
        'DELETE': role.ADMIN,
    }
复制代码

get_required_permissions 改写为根据最小权限返回一个权限列表:

def get_required_permissions(perms_map, allowed_methods, method):
    """ 接收 APIView 配置的 min_perms_map 以及发送的请求方法(Method),返回容许请求的 角色列表。若是 APIView 中未对 method 进行权限配置,则视为全部角色都用户该 method 的权限。 """
    if method not in perms_map:
        if method not in allowed_methods:
            raise exceptions.MethodNotAllowed(method)
        return list(range(1, role.GUEST + 1))
    return list(range(1, perms_map[method] + 1))
复制代码

has_perms 方法在 Django的 User 数据模型中定义,没法重写。直接新建一个普通方法 has_perms 去获取本次请求对应的权限是否符合:

def has_perms(request, perms: list):
    """判断用户在项目中的权限"""
    try:
        role = get_or_create_role(request.user.pk, request.session)
        if not role:
            return False
        if not perms or role in perms:
            return True
        return False
    except:
        return False
复制代码

设置为默认 permission 类

由于 RolePermission 类在咱们的应用生成以后才初始化,所以不能配置在 settings.py 中。

个人解决方案是重写一个 MyAPIView 类,继承自 DRF 的 APIView 类。在该类中配置:

from rest_framework.views import APIView
from myapp.permissions import RolePermissions

class MyAPIView(APIView):
    permission_class = [RolePermissions]
复制代码

而后每个接口都继承自 MyAPIView 便可。

单元测试

在不关注角色的用例中,咱们能够为 MyAPIView 类写一个开关,例如在变量 RUN_TESTTrue 时不配置 permission_class 来绕过权限判断的限制:

class MyAPIView(APIView):
    if RUN_TEST is False:
        permission_class = [RolePermissions]
复制代码

总结

业务不一样,基于角色的权限控制(RBAC)也有不一样的实现方案。对于更精细化的权限管理,还须要设计更复杂的权限关系。选择适合本身业务的方案。

相关文章
相关标签/搜索