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)也有不一樣的實現方案。對於更精細化的權限管理,還須要設計更復雜的權限關係。選擇適合本身業務的方案。

相關文章
相關標籤/搜索