权限系统在后台中不可避免,本文分享一下咱们的权限系统实现方案。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)也有不一样的实现方案。对于更精细化的权限管理,还须要设计更复杂的权限关系。选择适合本身业务的方案。