本文探讨了如何在Django REST框架(DRF)中构建自定义权限类。
--
Django REST框架的权限系列。
目标
在本文结束时,你应该能够。
- 创建自定义权限类
- 解释何时在你的自定义权限类中使用
has_permission和has_object_permission - 当一个权限被拒绝时返回一个自定义的错误信息
- 使用 AND, OR, 和 NOT 操作符来组合和排除权限类
如果你的应用程序有一些特殊的要求, 而内置的权限类不能满足这些要求, 是时候开始建立你自己的自定义权限了.
创建自定义权限允许你根据用户是否经过验证,请求方法,用户所属的组,对象属性,IP地址......或它们的任何组合来设置权限。
所有的权限类,无论是自定义的还是内置的,都是从BasePermission 类中延伸出来的。
class BasePermission(metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""
def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
BasePermission 有两个方法, 和 ,它们都返回 。权限类覆盖了其中一个或两个方法,has_permission has_object_permission True有条件地返回 。如果你不覆盖这些方法,它们将总是返回 ,授予无限的访问权。True True
关于
has_permission与has_object_permission的更多信息,请务必查看本系列的第一篇文章:Django REST框架中的权限。
按照惯例,你应该把自定义权限放在permissions.py文件中。这只是一个惯例,所以如果你需要以不同的方式组织你的权限,你就不必这样做了。
和内置权限一样,如果在视图中使用的任何一个权限类从has_permission 或has_object_permission 返回False ,就会产生一个PermissionDenied 异常。要改变与异常相关的错误信息,你可以直接在你的自定义权限类上设置一个消息属性。
有了这些,我们来看看一些例子。
自定义权限例子
用户属性
你可能想根据用户的属性给不同的用户以不同的访问级别 -- 即他们是对象的创建者还是工作人员?
比方说,你不希望工作人员能够编辑对象。下面是这种情况下的自定义权限类的样子。
# permissions.py
from rest_framework import permissions
class AuthorAllStaffAllButEditOrReadOnly(permissions.BasePermission):
edit_methods = ("PUT", "PATCH")
def has_permission(self, request, view):
if request.user.is_authenticated:
return True
def has_object_permission(self, request, view, obj):
if request.user.is_superuser:
return True
if request.method in permissions.SAFE_METHODS:
return True
if obj.author == request.user:
return True
if request.user.is_staff and request.method not in self.edit_methods:
return True
return False
这里,AuthorAllStaffAllButEditOrReadOnly 类扩展了BasePermission 并重写了has_permission 和has_object_permission.
has_permission。
在has_permission ,只有一件事被检查。如果用户被认证了。如果没有,NotAuthenticated 异常就会被引发,并且访问被拒绝。
has_object_permission。
因为你不应该限制超级用户的访问,所以第一个检查--request.user.is_superuser --授予超级用户访问权。
接下来,我们检查请求方法是否是 "安全 "的方法之一 --request.method in permissions.SAFE_METHODS 。安全方法在rest_framework/permissions.py中定义。
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
这些方法对对象没有影响;它们只能读取它。
乍一看,似乎SAFE_METHODS 的检查应该在has_permission 方法中。如果你只检查请求方法,那么这就是它应该出现的地方。但在这种情况下,其他检查就不会被执行。
因为我们想在方法是安全的方法之一**,或者用户是对象的作者,或者**用户是工作人员的时候授予访问权,所以我们需要在同一层面上进行检查。换句话说,由于我们不能在has_permission 层面上检查所有者,我们需要在has_object_permission 层面上检查一切。
最后一种可能性是,用户是工作人员。除了我们定义的edit_methods ,他们可以使用所有的方法。
最后,再回头看看类的名称:AuthorAllStaffAllButEditOrReadOnly 。你应该总是尽量把权限类的名字命名得信息量大一些。
请记住,
has_object_permission对于列表视图(不管你是从哪个视图扩展过来的)或者当请求方法是POST(因为对象还不存在)时,永远不会被执行。
你使用自定义权限类的方式与内置权限相同。
# views.py
from rest_framework import viewsets
from .models import Message
from .permissions import AuthorAllStaffAllButEditOrReadOnly
from .serializers import MessageSerializer
class MessageViewSet(viewsets.ModelViewSet):
permission_classes = [AuthorAllStaffAllButEditOrReadOnly] # Custom permission class used
queryset = Message.objects.all()
serializer_class = MessageSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
对象的作者对它有完全的访问权。同时,一个工作人员可以删除该对象,但不能编辑它。
而一个被认证的用户可以查看该对象,但不能编辑或删除它。
对象属性
尽管我们在前面的例子中简要地提到了对象的属性,但重点更多的是放在用户的属性上(例如,对象的作者)。在这个例子中,我们将重点讨论对象的属性。
对象的一个或多个属性如何能对权限产生影响?
- 就像前面的例子一样,你可以只限制对象的所有者的访问。你也可以限制对所有者所属的组的访问。
- 对象可能有一个过期日期,所以你可以限制只有某些用户可以访问超过n年的对象。
- 你可以让DELETE作为一个标志来实现(这样它就不会真正从数据库中删除)。然后你可以阻止对带有删除标志的对象的访问。
比方说,你想限制除超级用户之外的所有人对超过10分钟的对象的访问。
# permissions.py
from datetime import datetime, timedelta
from django.utils import timezone
from rest_framework import permissions
class ExpiredObjectSuperuserOnly(permissions.BasePermission):
def object_expired(self, obj):
expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10))
return obj.created < expired_on
def has_object_permission(self, request, view, obj):
if self.object_expired(obj) and not request.user.is_superuser:
return False
else:
return True
在这个权限类中,has_permission 方法没有被重载 -- 所以它将总是返回True 。
由于唯一重要的属性是对象的创建时间,检查发生在has_object_permission (因为我们不能访问对象的属性,在has_permission )。
因此,如果一个用户想访问过期的对象,就会引发异常PermissionDenied 。
同样,和前面的例子一样,我们可以在has_permission 中检查用户是否是超级用户,但如果他们不是,对象的属性就不会被检查。
请注意这个错误信息。它的信息量不大。用户不知道为什么他们的访问被拒绝了。我们可以通过给我们的权限类添加一个message 属性来创建一个自定义错误信息。
class ExpiredObjectSuperuserOnly(permissions.BasePermission):
message = "This object is expired." # custom error message
def object_expired(self, obj):
expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10))
return obj.created < expired_on
def has_object_permission(self, request, view, obj):
if self.object_expired(obj) and not request.user.is_superuser:
return False
else:
return True
现在用户可以看到权限被拒绝的确切原因。
合并和排除权限类
通常情况下, 当使用一个以上的权限类时, 你会像这样在视图中定义它们。
permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]
这种方法将它们结合起来,只有当所有的类都返回True ,才会授予权限。
从DRF3.9.0版本开始,你也可以使用AND (&)或OR (|)逻辑运算符来组合多个类。另外,从3.9.2开始,支持NOT (~)运算符。
这些运算符不限于自定义权限类。它们也可以用于内置权限。
与其创建许多彼此相似的复杂权限类, 你可以创建更简单的类并将它们与上述运算符结合起来。
例如,你可能对不同的组的组合有不同的权限。比方说,你想要以下的权限。
- A组或B组的权限
- 对B组或C组的权限
- 对B和C两组成员的权限
- 对除A以外的所有组的权限
虽然四个权限类别看起来不多,但这并不能很好地扩展。如果你有八个不同的组--A、B、C、D、E、F、G?这将很快发展到无法理解和维护的地步。
你可以简化它,通过首先为A、B和C组创建权限类,将它们与运算符结合起来。然后,你可以像这样实现它们。
permission_classes = [PermGroupA | PermGroupB]permission_classes = [PermGroupB | PermGroupC]permission_classes = [PermGroupB & PermGroupC]permission_classes = [~PermGroupA]
当涉及到OR (
|)时,事情会变得有点复杂。错误往往会从缝隙中漏掉。更多内容,请回顾关于权限的讨论。允许权限组成拉动请求。
和操作符
AND是权限类的默认行为, 通过使用, 实现:
permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]
它也可以用& 。
permission_classes = [IsAuthenticated & IsStaff & SomeCustomPermissionClass]
OR 运营商
用OR (|), 当任何一个权限类返回True ,该权限就被授予。你可以使用OR运算符来提供用户被授予权限的多种可能性。
让我们看一个例子,对象的所有者或工作人员可以编辑或删除该对象。
我们将需要两个类。
IsStaff如果该用户为工作人员,则返回Trueis_staffIsOwner返回 ,如果该用户与Trueobj.author
代码。
class IsStaff(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.is_staff:
return True
return False
def has_object_permission(self, request, view, obj):
if request.user.is_staff:
return True
return False
class IsOwner(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.is_authenticated:
return True
return False
def has_object_permission(self, request, view, obj):
if obj.author == request.user:
return True
return False
这里有相当多的冗余,但这是必要的。
为什么呢?
-
对于覆盖列表视图
同样,列表视图不检查
has_object_permission。然而,每一个创建的权限都需要是独立的。你不应该创建一个权限类,需要与另一个权限类结合来覆盖列表视图。IsOwner限制对has_permission中的认证用户的访问 -- 这样,如果IsOwner是唯一使用的类,对API的访问仍然受到控制。 -
这两种方法默认都会返回
True当使用OR时,如果你不提供
has_object_permission方法,用户将有机会访问该对象,尽管他们不应该。注意。
-
如果你在
IsOwner类上省略了has_permission,任何人都可以在列表上看到或创建。 -
如果你在
IsStaff上省略了has_object_permission,并将其与IsOwner和or结合起来,其中一个就会返回True。这样一来,一个既不是所有者也不是工作人员的注册用户,将能够改变内容。
-
现在,当我们把我们的权限类设计好后,很容易把它们结合起来。
from rest_framework import viewsets
from .models import Message
from .permissions import IsStaff, IsOwner
from .serializers import MessageSerializer
class MessageViewSet(viewsets.ModelViewSet):
permission_classes = [IsStaff | IsOwner] # or operator used
queryset = Message.objects.all()
serializer_class = MessageSerializer
在这里,我们允许工作人员或对象的所有者来改变或删除它。
IsOwner 对列表视图的唯一要求,是用户要经过认证。这意味着,经过认证的用户,如果不是工作人员,将能够创建对象。
NOT操作符
NOT操作符的结果与定义的权限类别完全相反。换句话说, 权限被授予所有用户, 除了权限类的用户。
比方说,你有三组用户。
每个组都应该能够访问只针对其特定组的API端点。
这里有一个权限类,只授予财务组的成员访问权。
class IsFinancesMember(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.groups.filter(name="Finances").exists():
return True
现在,假设你有一个新的视图,是为所有不属于财务组的用户准备的。你可以使用NOT操作符来实现这一点。
from rest_framework import viewsets
from .models import Message
from .permissions import IsFinancesMember
from .serializers import MessageSerializer
class MessageViewSet(viewsets.ModelViewSet):
permission_classes = [~IsFinancesMember] # using not operator
queryset = Message.objects.all()
serializer_class = MessageSerializer
所以,只有财务组的成员不能访问。
请注意!如果你只使用NOT运算符,那么就会出现以下问题如果你只使用NOT操作符,其他所有人都将被允许访问,包括未认证的用户如果这不是你想做的,你可以通过添加另一个类来解决这个问题,就像这样。
permission_classes = [~IsFinancesMember & IsAuthenticated]
圆括号
在permission_classes ,你也可以使用括号(())来控制哪个表达式被优先解决。
快速的例子。
class MessageViewSet(viewsets.ModelViewSet):
permission_classes = [(IsFinancesMember | IsTechMember) & IsOwner] # using parentheses
queryset = Message.objects.all()
serializer_class = MessageSerializer
在这个例子中,(IsFinancesMember | IsTechMember) 将首先被解析。然后,其结果将被用于& IsOwner -- 例如,ResultsFromFinancesOrTech & IsOwner 。这意味着技术组或财务组的成员,并且是该对象的所有者的用户将被授予访问权。
总结
尽管有广泛的内置权限类, 但在某些情况下它们并不能满足你的需求.这时自定义权限类就派上用场了。
对于自定义权限类, 你必须覆盖以下一个或两个方法。
has_permissionhas_object_permission
如果在has_permission 方法中没有授予权限,那么在has_object_permission 中写什么并不重要 -- 该权限被拒绝。如果你没有覆盖其中的一个(或两个),你需要考虑到,在默认情况下,该方法将总是返回True 。
你可以用AND, OR, 和NOT操作符来组合和排除权限类。你甚至可以用括号来决定权限解析的顺序。