權限系統在後臺中不可避免,本文分享一下咱們的權限系統實現方案。python
在分享前先簡單介紹一下咱們的平臺業務。咱們是質量部,咱們的平臺對接了多個業務部門,所以須要實現:git
不一樣用戶在不一樣部門的項目中擁有一種角色,每種角色對不一樣的接口有不一樣的操做權限,例如:github
以上就是簡化後的權限系統的需求,下面講講實現方案。數據庫
在 Django - 模型序列化返回天然主鍵值 一文中咱們瞭解過 DRF 的序列化模塊,除了序列化,DRF 還封裝好了不少好用的功能,好比咱們目前平臺的 APIView
就是繼承自 DRF 的 APIView
類,還有分頁類(Pagination)和權限控制類(Permission)等等。django
咱們實現權限控制的方案就借鑑了 DRF 的 DjangoModelPermission
類。bash
Django 的權限模塊其實已經有
User
,Group
,Permission
數據模型以及關聯關係,之因此不用官方的權限也不直接用 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 中,減小頻繁查詢數據庫的過程。
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
...
複製代碼
權限主要指對各接口發送到不一樣請求方法的操做權限。
接口-請求方法-角色關係:
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
複製代碼
由於 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_TEST
爲 True
時不配置 permission_class
來繞過權限判斷的限制:
class MyAPIView(APIView):
if RUN_TEST is False:
permission_class = [RolePermissions]
複製代碼
業務不一樣,基於角色的權限控制(RBAC)也有不一樣的實現方案。對於更精細化的權限管理,還須要設計更復雜的權限關係。選擇適合本身業務的方案。