Django REST框架中的自定义权限类

816 阅读9分钟

本文探讨了如何在Django REST框架(DRF)中构建自定义权限类。

--

Django REST框架的权限系列。

目标

在本文结束时,你应该能够。

  1. 创建自定义权限类
  2. 解释何时在你的自定义权限类中使用has_permissionhas_object_permission
  3. 当一个权限被拒绝时返回一个自定义的错误信息
  4. 使用 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_permissionhas_object_permission 的更多信息,请务必查看本系列的第一篇文章:Django REST框架中的权限。

按照惯例,你应该把自定义权限放在permissions.py文件中。这只是一个惯例,所以如果你需要以不同的方式组织你的权限,你就不必这样做了。

和内置权限一样,如果在视图中使用的任何一个权限类从has_permissionhas_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_permissionhas_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)

对象的作者对它有完全的访问权。同时,一个工作人员可以删除该对象,但不能编辑它。

而一个被认证的用户可以查看该对象,但不能编辑或删除它。

对象属性

尽管我们在前面的例子中简要地提到了对象的属性,但重点更多的是放在用户的属性上(例如,对象的作者)。在这个例子中,我们将重点讨论对象的属性。

对象的一个或多个属性如何能对权限产生影响?

  1. 就像前面的例子一样,你可以只限制对象的所有者的访问。你也可以限制对所有者所属的组的访问。
  2. 对象可能有一个过期日期,所以你可以限制只有某些用户可以访问超过n年的对象。
  3. 你可以让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 (~)运算符。

这些运算符不限于自定义权限类。它们也可以用于内置权限。

与其创建许多彼此相似的复杂权限类, 你可以创建更简单的类并将它们与上述运算符结合起来。

例如,你可能对不同的组的组合有不同的权限。比方说,你想要以下的权限。

  1. A组或B组的权限
  2. 对B组或C组的权限
  3. 对B和C两组成员的权限
  4. 对除A以外的所有组的权限

虽然四个权限类别看起来不多,但这并不能很好地扩展。如果你有八个不同的组--A、B、C、D、E、F、G?这将很快发展到无法理解和维护的地步。

你可以简化它,通过首先为A、B和C组创建权限类,将它们与运算符结合起来。然后,你可以像这样实现它们。

  1. permission_classes = [PermGroupA | PermGroupB]
  2. permission_classes = [PermGroupB | PermGroupC]
  3. permission_classes = [PermGroupB & PermGroupC]
  4. permission_classes = [~PermGroupA]

当涉及到OR (|)时,事情会变得有点复杂。错误往往会从缝隙中漏掉。更多内容,请回顾关于权限的讨论。允许权限组成拉动请求。

和操作符

AND是权限类的默认行为, 通过使用, 实现:

permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]

它也可以用&

permission_classes = [IsAuthenticated & IsStaff & SomeCustomPermissionClass]

OR 运营商

用OR (|), 当任何一个权限类返回True ,该权限就被授予。你可以使用OR运算符来提供用户被授予权限的多种可能性。

让我们看一个例子,对象的所有者或工作人员可以编辑或删除该对象。

我们将需要两个类。

  1. IsStaff 如果该用户为工作人员,则返回True is_staff
  2. IsOwner 返回 ,如果该用户与True obj.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

这里有相当多的冗余,但这是必要的。

为什么呢?

  1. 对于覆盖列表视图

    同样,列表视图不检查has_object_permission 。然而,每一个创建的权限都需要是独立的。你不应该创建一个权限类,需要与另一个权限类结合来覆盖列表视图。IsOwner 限制对has_permission 中的认证用户的访问 -- 这样,如果IsOwner 是唯一使用的类,对API的访问仍然受到控制。

  2. 这两种方法默认都会返回True

    当使用OR时,如果你不提供has_object_permission 方法,用户将有机会访问该对象,尽管他们不应该。

    注意。

    • 如果你在IsOwner 类上省略了has_permission ,任何人都可以在列表上看到或创建。

    • 如果你在IsStaff 上省略了has_object_permission ,并将其与IsOwneror 结合起来,其中一个就会返回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_permission
  • has_object_permission

如果在has_permission 方法中没有授予权限,那么在has_object_permission 中写什么并不重要 -- 该权限被拒绝。如果你没有覆盖其中的一个(或两个),你需要考虑到,在默认情况下,该方法将总是返回True

你可以用AND, OR, 和NOT操作符来组合和排除权限类。你甚至可以用括号来决定权限解析的顺序。