Django REST框架 - 按方法分离权限

94

我正在使用Django REST Framework编写API,想知道在使用类视图时是否可以针对每个方法指定权限。

阅读文档后, 我发现如果您正在编写基于函数的视图,只需在要受权限保护的视图函数上使用@permission_classes装饰器即可轻松实现。但是,如果使用APIView类的CBV,则无法使用相同的方法进行操作,因为需要使用permission_classes属性来指定整个类的权限,但此时将应用到所有类方法(例如getpostput ...)。

因此,是否可能使用CBV编写API视图并为每个方法指定不同的权限?


你可以为每个视图创建一个单独的视图,或者你可以重写视图中的get/post/put方法并编写自己的权限。 - Zack Argyle
11个回答

80

权限应用于整个View类,但您可以在授权决策中考虑请求的某些方面(例如方法,如GET或POST)。

以内置的IsAuthenticatedOrReadOnly为例:

SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']

class IsAuthenticatedOrReadOnly(BasePermission):
    """
    The request is authenticated as a user, or is a read-only request.
    """

    def has_permission(self, request, view):
        if (request.method in SAFE_METHODS or
            request.user and
            request.user.is_authenticated()):
            return True
        return False

抱歉回复晚了。谢谢,Kevin。你的答案非常完美。这里有一个IsAuthenticatedOrReadOnly权限类,可以使用SAFE_METHODS - José L. Patiño
非常好,也适用于仅限POST的API,例如第三方联盟通过其创建线索,但如何防止整个线索列表被列出呢? - Oleg Belousov
2
在Django 3+中(不确定之前的版本),request.user.is_authenticated()实际上是request.user.is_authenticatedis_authenticated不再是一个方法,而是一个布尔值。 - Séraphin
你可以使用 from rest_framework.permissions import SAFE_METHODS 来代替自己重新定义 SAFE_METHODS - phoenix

55

在使用基于类的视图时,我遇到了同样的问题,因为我的权限逻辑相当复杂,取决于请求方法。

我想出的解决办法是使用第三方的'rest_condition'应用程序,该应用程序在此页面底部列出:

http://www.django-rest-framework.org/api-guide/permissions

https://github.com/caxap/rest_condition

我只是将权限流程逻辑拆分成每个分支,具体取决于请求方法。

from rest_condition import And, Or, Not

class MyClassBasedView(APIView):

    permission_classes = [Or(And(IsReadOnlyRequest, IsAllowedRetrieveThis, IsAllowedRetrieveThat),
                             And(IsPostRequest, IsAllowedToCreateThis, ...),
                             And(IsPutPatchRequest, ...),
                             And(IsDeleteRequest, ...)]

因此,'Or'确定了应根据请求方法运行哪个权限分支,而'And'包装与接受的请求方法相关的权限,因此所有权限必须通过才能授予权限。您还可以在每个流程中混合使用'Or'、'And'和'Not'以创建更复杂的权限。

要运行每个分支的权限类只需像这样:

class IsReadyOnlyRequest(permissions.BasePermission):

    def has_permission(self, request, view):
        return request.method in permissions.SAFE_METHODS


class IsPostRequest(permissions.BasePermission):

    def has_permission(self, request, view):
        return request.method == "POST"


... #You get the idea

请随意尝试此链接:https://github.com/Pithikos/rest-framework-roles - Pithikos
2
DRF已添加类似于以下功能: https://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy “只要它们继承自rest_framework.permissions.BasePermission,就可以使用标准的Python位运算符组合权限...注意:它支持&(和),|(或)和~(not)。 ” 因此,您可以编写像permission_classes = [IsAlienFromSpace&IsFriendly,IsAuthenticated | ReadOnly]这样的权限。 - Joshua Swain

21

2020年3月30日更新:我的原始解决方案仅修补了对象权限,而不是请求权限。我在下面提供了更新,以使其与请求权限一起使用。

我知道这是一个旧问题,但最近我遇到了同样的问题并想分享我的解决方案(因为被接受的答案不完全符合我的需求)。@GDorn的回答给了我正确的方向,但它只适用于ViewSet,因为涉及到self.action

我创建了自己的装饰器来解决这个问题:

def method_permission_classes(classes):
    def decorator(func):
        def decorated_func(self, *args, **kwargs):
            self.permission_classes = classes
            # this call is needed for request permissions
            self.check_permissions(self.request)
            return func(self, *args, **kwargs)
        return decorated_func
    return decorator

与内置装饰器不同的是,我的装饰器不会在函数上设置permission_classes属性,而是包装调用并在被调用的视图实例上设置权限类。这样,普通的get_permissions()不需要任何更改,因为它只依赖于self.permission_classes

要使用请求权限,我们确实需要从装饰器中调用check_permission(),因为它最初是在initial()中调用的,在那之前permission_classes属性已经被修补了。

注意:通过装饰器设置的权限是唯一适用于对象权限的权限,但对于请求权限,它们是除了类范围权限之外的权限,因为在请求方法被调用之前始终会检查这些权限。如果您想仅针对每个方法指定所有权限,请在类上设置permission_classes = []

示例用例:

from rest_framework import views, permissions

class MyView(views.APIView):
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)  # used for default APIView endpoints
    queryset = MyModel.objects.all()
    serializer_class = MySerializer


    @method_permission_classes((permissions.IsOwnerOfObject,))  # in addition to IsAuthenticatedOrReadOnly
    def delete(self, request, id):
        instance = self.get_object()  # ...

希望这能帮助到遇到同样问题的人!


14

我遇到了这个问题,非常想使用@permission_classes装饰器来标记一些具有特定权限的自定义视图方法。最终我想出了一个mixin:

class PermissionsPerMethodMixin(object):
    def get_permissions(self):
        """
        Allows overriding default permissions with @permission_classes
        """
        view = getattr(self, self.action)
        if hasattr(view, 'permission_classes'):
            return [permission_class() for permission_class in view.permission_classes]
        return super().get_permissions()

一个使用案例示例:

from rest_framework.decorators import action, permission_classes  # other imports elided

class MyViewset(PermissionsPerMethodMixin, viewsets.ModelViewSet):
    permission_classes = (IsAuthenticatedOrReadOnly,)  # used for default ViewSet endpoints
    queryset = MyModel.objects.all()
    serializer_class = MySerializer

    @action(detail=False, methods=['get'])
    @permission_classes((IsAuthenticated,))  # overrides IsAuthenticatedOrReadOnly
    def search(self, request):
        return do_search(request)  # ...

7

我遇到了类似的问题。

我想允许未经身份验证的POST请求,但禁止未经身份验证的GET请求。

未经身份验证的公众成员可以提交一个项目,但只有经过身份验证的管理员用户才能检索所提交的项目列表。

因此,我构建了一个自定义的权限类 - UnauthenticatedPost - 用于POST请求,然后将权限类列表设置为 IsAuthentictaedUnauthenticatedPost

请注意,我只允许使用http_method_names = ['get', 'post']来设置可允许的方法,即获取和提交。

from django.http import HttpResponse
from rest_framework import viewsets
from rest_framework.permissions import BasePermission, IsAuthenticated
from MyAPI.serializers import MyAPISerializer
from MyAPI.models import MyAPI


class UnauthenticatedPost(BasePermission):
    def has_permission(self, request, view):
        return request.method in ['POST']


class MyAPIViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated|UnauthenticatedPost]
    queryset = MyAPI.objects.all().order_by('-TimeSubmitted')
    serializer_class = MyAPISerializer
    http_method_names = ['get', 'post']

6
这个问题涉及到APIView实例,但对于任何想要使用ViewSets内的@action修饰器进行每个方法权限覆盖的人,可以参考以下内容:

class SandwichViewSet(ModelViewSet):
  permission_classes = [IsAuthenticated]

  @action(..., permission_classes=[CanSeeIngredients])
  def retrieve__ingredients(self, request):
    ...

这个可以工作吗?我无法使其工作,它只是被忽略了。我进行了调试,并且它确实覆盖了权限类,但我的端点仍然可以被另一个用户访问。我应该以某种方式调用super或其他东西吗?@Adam - Viktor Vostrikov
3
好的,它是有效的。不过你需要调用self.get_object()来触发权限。 - Viktor Vostrikov

6
如果您使用 ViewSetModelViewSet,我认为重写 get_permissions 方法就可以解决问题了。
看一下djoser是如何处理这个问题的。
示例:
class UserViewSet(viewsets.ModelViewSet):
    permission_classes = settings.PERMISSIONS.user  # default

    def get_permissions(self):
        if self.action == "activation":  # per action
            self.permission_classes = settings.PERMISSIONS.activation
        return super().get_permissions()

    @action(["post"], detail=False)  # action
    def activation(self, request, *args, **kwargs):
        pass

    

2

在处理GET、PUT和POST请求时,我们遇到了相同的挑战,需要不同的权限。我们通过自定义权限类来解决这个问题:

from rest_framework import permissions

class HasRequiredPermissionForMethod(permissions.BasePermission):
    get_permission_required = None
    put_permission_required = None
    post_permission_required = None

    def has_permission(self, request, view):
        permission_required_name = f'{request.method.lower()}_permission_required'
        if not request.user.is_authenticated:
            return False
        if not hasattr(view, permission_required_name):
            view_name = view.__class__.__name__
            self.message = f'IMPLEMENTATION ERROR: Please add the {permission_required_name} variable in the API view class: {view_name}.'
            return False

        permission_required = getattr(view, permission_required_name)
        if not request.user.has_perm(permission_required):
            self.message = f'Access denied. You need the {permission_required} permission to access this service with {request.method}.'
            return False

        return True

我们在 API 中使用它,像这样:

class MyAPIView(APIView):
    permission_classes = [HasRequiredPermissionForMethod]
    get_permission_required = 'permission_to_read_this'
    put_permission_required = 'permission_to_update_this'
    post_permission_required = 'permission_to_create_this'

    def get(self, request):
        # impl get

    def put(self, request):
        # impl put

    def post(self, request):
        # impl post

1

所以,我为此制作了一个mixin。你只需要继承这个mixin。

class FooViewSet(ModelViewSet, PermissionByAction):

    queryset = Foo.objects.all()
    serializer_class = FooSerializer

    permission_classes_by_action = {
        'create': [IsAuthenticated],
        'list': [AllowAny],
        'retrieve': [AllowAny],
        'destroy': [IsOwner | IsAdmin,],
    }

这是一个混入:

from rest_framework.settings import api_settings
class PermissionByAction(object):

    permission_classes_by_action : dict = {
        'create': api_settings.DEFAULT_PERMISSION_CLASSES,
        'list': api_settings.DEFAULT_PERMISSION_CLASSES,
        'retrieve': api_settings.DEFAULT_PERMISSION_CLASSES,
        'destroy': api_settings.DEFAULT_PERMISSION_CLASSES,
    }

    def get_permissions(self):
        permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
    
        if self.action == "list":
            permission_classes = self.permission_classes_by_action['list']
        
        elif self.action == "create":
            permission_classes = self.permission_classes_by_action['create']
        
        elif self.action == "retrieve":
            permission_classes = self.permission_classes_by_action['retrieve']
        
        elif self.action == "destroy":
            permission_classes = self.permission_classes_by_action['destroy']

        return [permission() for permission in permission_classes]

1
我写下我的解决方案,希望能帮到别人。
据我所知,处理APIView类中的权限有两种方法:

  1. 静态地为APIView.permission_classes赋值一个适当的Permission类(例如扩展BasePermission
  2. 动态地在APIView中决定Permission实例(覆盖APIView.get_permission()

APIView检查从.get_permission()返回的权限。
.get_permission().permission_classes实例化Permission

在我的情况下,我只需要预定义的Permission但是根据方法而定。所以我选择了后一种方法。

class TokenView(APIView):
    authentication_classes = [TokenAuthentication]
    
    // return instances of Permission classes
    def get_permissions(self, *args, **kwargs):
        if self.request.method in ['DELETE']:
            return [IsAuthenticated()]
        else:
            return []

    def post(self, request, *args, **kwargs):
        username = request.data["username"]
        password = request.data["password"]
        user = authenticate(username=username, password=password)
        token, created = Token.objects.get_or_create(user=user)
        return Response({"token": token.key}, status=status.HTTP_200_OK)

    def delete(self, request, *args, **kwargs):
        user = request.user
        user.auth_token.delete()
        return Response({"success", status.HTTP_200_OK})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接