Django Rest Framework 4:认证、权限和限流

导读:本篇文章讲解 Django Rest Framework 4:认证、权限和限流,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目前,我们的 API 对谁可以编辑或删除没有任何限制。我们希望有更高级的行为,以确保:

  • 代码片段始终与创建者相关联。
  • 只有通过身份验证的用户可以创建片段。
  • 只有代码片段的创建者可以更新或删除它。
  • 未经身份验证的请求应具有完全只读权限。

其实这些要求也是各种资源普遍应该拥有的权限,要操作这些资源,前提就是能通过认证,甚至还会被一些限流机制所限制。

一,django 中的用户、权限与认证

Django 认证系统处理验证和授权:

  • 验证检查用户是否是他们的用户
  • 授权决定已验证用户能做什么

这里的认证用于指代这两个任务。这两个任务都依赖于 User 模块。

  • User 通过从 PermissionsMixin 中继承的 groups 字段与 Group 产生多对多关联。
  • User 通过从 PermissionsMixin 中继承的 user_permissions 字段与 Permission 产生多对多关联。

django 默认为每个模型创建 view、add、change 和 delete 这四项权限。但也允许直接操作 Permission或在模型 Meta 中创建额外的权限。

  • 通常会在管理后台进行用户分组以及权限分配相关的操作。

在登录、注销等操作中可直接使用认证系统提供的验证函数、装饰器、Mixin等手段对用户进行身份验证、权限验证和一些其他的限制性操作。

二,进行认证和权限开发的准备工作

(一)添加用户

DRF 的权限与认证也都依赖于用户模型,为了让代码片段始终与创建者相关联,首先就需要修改 Snippet 模型,让它关联用户模型(这里我们直接使用 django 内置的 User 模型)并能够存储代码的高亮显示的HTML内容。看看代码内容👨‍💻:

...
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight


class Snippet(models.Model):
	...
    owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
    highlighted = models.TextField()
	...
	def save(self, *args, **kwargs):
	    """
	    保存使用`pygments`库创建的能高亮的HTML格式的代码段。
	    """
	    lexer = get_lexer_by_name(self.language)
	    linenos = self.linenos and 'table' or False
	    options = self.title and {'title': self.title} or {}
	    formatter = HtmlFormatter(style=self.style, linenos=linenos,
	                              full=True, **options)
	    self.highlighted = highlight(self.code, lexer, formatter)
	    super(Snippet, self).save(*args, **kwargs)

Snippet 模型有字段增删的话,就记得要修改对应的的序列化器,我们选择使用 ReadOnlyField 字段实现 owner 外键的序列化并让用户字段只读看看代码内容👨‍💻:

class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style', 'owner']

为了保证数据库数据的一致性,删除数据库并重新进行数据迁移。看看代码内容👨‍💻:

$ rm -f tmp.db db.sqlite3
$ rm -r snippet/migrations
$ python manage.py makemigrations snippet
$ python manage.py migrate

还要创建几个不同的超级用户用户,以用于后续测试 API。

(二)用户序列化器

既然都创建了用户了,不如干脆就实现其序列化,也算是提供了丰富的 API 了。

先来实现用户序列化器。看看代码内容👨‍💻:

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    snippets = SnippetSerializer(many=True)

    class Meta:
        model = User
        fields = ('id', 'username', 'snippets')

然后使用通用视图类 ListAPIViewRetrieveAPIView 将用户展示为只读视图。看看代码内容👨‍💻:

class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

最后在 app 中添加路由。看看代码内容👨‍💻:

urlpatterns = [
    ...
    path('users/', views.UserList.as_view()),
    path('users/<int:pk>/', views.UserDetail.as_view()),
]
  • 这些数据都是在管理后台中预添加的。

(三)关联 Snippet 与用户

但是,目前这种用户序列化器中嵌套 Snippet 序列化器的关系是与模型中的关联关系相反的,也就导致无法在保存 Snippet 时附加所需要的 User 。

尽管可以重写 SnippetSerializer 的 .create() 方法来实现可写的嵌套,举个例子🌰:

class UserSerializer(serializers.ModelSerializer):
    profile = ProfileSerializer()

    class Meta:
        model = User
        fields = ('username', 'email', 'profile')

    def create(self, validated_data):
        profile_data = validated_data.pop('profile')
        user = User.objects.create(**validated_data)		# 先创保存主表数据
        Profile.objects.create(user=user, **profile_data)	# 紧接着保存从表数据
        return user

但更加方便的方法就是重写CreateModelMixin.perform_create()方法来事先从请求中获取用户数据并保存。看看代码内容👨‍💻:

class SnippetList(mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
	...
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

现在,代码片段与用户是真正相关联的了,下一步就是确保只有经过身份验证的用户才能创建,更新和删除代码片段。

(四)给 Browsable API 添加登陆

首先在 API 操作页面创建用户登陆功能,简单使用 rest_framework.urls 就能实现:

from django.urls import path, include

urlpatterns = [
    ...
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

路由名自定义,但命名空间必须是'rest_framework'

再次打开浏览器并刷新页面,你将在页面右上角看到一个“登录”链接。

三,认证系统、权限系统与限流系统协同工作概述

(一)从 APIVview 的源码开始

先回顾一下路由中调用 APIVview.as_view 方法后是怎样产生 Request 对象的:
在这里插入图片描述

(二)身份验证、权限管理和请求限流

权限系统与认证系统就将基于 Request 对象进行权限限制与请求验证。

再看看产生 Request 对象后它又会做什么:
在这里插入图片描述
主要就是内容协商、版本控制、请求处理和创建响应。
在进行请求处理时主要又执行下面的操作:

  1. 认证系统根据验证后端对请求进行身份验证。
  2. 权限系统根据权限策略检查是否应允许请求。
  3. 限制系统根据限制策略执行如何限制请求。

这里我们就重点关注请求处理中的前两项。

具体来看看 Request 对象:

  • 权限权限系统通过检查请求中的 request.userrequest.auth
    信息来确定传入的请求是否应被允许,认证系统也通过它们进行身份验证。

在这里插入图片描述

四,认证

身份验证是将传入的请求与一组标识凭据(例如请求来自的用户或令牌)相关联的机制。然后,权限系统和限流系统可以使用这些凭据来确定是否应允许该请求对视图的访问。

系统也使用以下请求的属性来进行身份验证:

  • request.user 属性,通常被设置为contrib.auth 包中 User 类的一个实例。
  • request.auth 属性,用于任何其他身份验证信息,例如,它可以用于表示请求签名的身份验证令牌。

REST framework 提供了一些开箱即用的身份验证类,并且还允许自定义。

(一)验证后端与验证结果

验证后端总是被定义为用于身份验证的类的列表,
REST framework 将尝试使用列表中的每个类进行身份验证。

  • 验证始终在视图的最开始进行。

还未验证的request.userrequest.auth 的值可以使用 UNAUTHENTICATED_USERUNAUTHENTICATED_TOKEN 配置进行修改。

如果验证成功,则使用成功完成验证的第一个类的返回值设置 request.userrequest.auth

如果没有类进行验证,request.user 将被设置成 django.contrib.auth.models.AnonymousUser的实例,request.auth 将被设置成 None。

当未经身份验证的请求被拒绝时,有下面两种不同的错误代码可使用:

  • HTTP 401 未认证。这个响应必须始终包括一个WWW-Authenticate头,指示客户端如何进行身份验证。
  • HTTP 403 无权限。响应不包括WWW-Authenticate。

具体使用哪种响应取决于验证后端中的类。虽然可以使用多种验证后端类,但仅允许使用一种类来确定响应的类型。

  • 在确定响应类型时,将使用视图上设置的第一个验证后端类。

当一个请求通过了验证但是被拒绝执行请求的权限时,不管验证类是什么,都要使用 403 Permission Denied 响应。

(二)设置验证后端

全局的默认身份验证方案为:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

也可以使用装饰器对具体函数视图使用指定的验证后端:

@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
    content = {
        'user': unicode(request.user),  # `django.contrib.auth.User` 实例。
        'auth': unicode(request.auth),  # None
    }
    return Response(content)

还可以为基于APIView的每个类视图或视图集指定身份验证方案:

from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
    authentication_classes = (SessionAuthentication, BasicAuthentication)
    permission_classes = (IsAuthenticated,)

    def get(self, request, format=None):
        content = {
            'user': unicode(request.user),  # `django.contrib.auth.User` 实例。
            'auth': unicode(request.auth),  # None
        }
        return Response(content)

(三)可用的验证后端类

REST framework 在 rest_framework/authentication.py 提供了一些内置的验证后端类,

1,BasicAuthentication

这个验证类使用HTTP 基本认证 针对用户的用户名和密码进行认证,但这个类通常只适用于测试。

如果认证成功 BasicAuthentication 提供以下信息。

  • request.user 将是一个 Django User 实例。
  • request.auth 将是 None。

被拒绝的未经身份验证的请求会返回使用适当WWW-Authenticate标头的HTTP 401 Unauthorized响应。例如:

WWW-Authenticate: Basic realm="api"
  • 字符串由 www_authenticate_realm 属性设置。

2,TokenAuthentication

使用简单的基于Token的HTTP认证方案:

(1)使用方法

  1. INSTALLED_APPS设置中包含rest_framework.authtoken
INSTALLED_APPS = (
    ...
    'rest_framework.authtoken'
)
  1. 执行数据迁移。
  2. 为你的用户创建令牌:
>>> from rest_framework.authtoken.models import Token
>>> token = Token.objects.create(user=...)
>>> token.key
  1. 对客户端进行身份验证,token需要包含在
    AuthorizationHTTP头中。密钥应该是以字符串”Token”为前缀,以空格分割的两个字符串。例如:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
  • 前缀字符串由 keyword 属性设置。

(2)认证结果

如果认证成功,TokenAuthentication 提供以下信息:

  • request.user 将是一个Django User 实例。
  • request.auth 将是一个rest_framework.authtoken.models.Token 实例。

被拒绝的未经身份验证的请求会返回使用适当WWW-Authenticate标头的HTTP 401 Unauthorized响应。例如:

WWW-Authenticate: Token

(3)生成Token

Rest Framework提供好几种生成Token的方式.

1,使用信号
场景一:在创建用户时为每一位用户创建Token,可以简单地捕获模型的post_save信号。举个例子🌰:

from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

# 注册信号接收器
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

场景二:为所有现有用户生成Token。举个例子🌰:

from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token

for user in User.objects.all():
    Token.objects.get_or_create(user=user)

2,暴露 api 端点
场景:为客户端提供根据给定用户名和密码获取Token的服务,只需将 obtain_auth_token 视图添加到路由中。举个例子🌰:

from rest_framework.authtoken import views
urlpatterns += [
    path('api-token-auth/', views.obtain_auth_token)
]
  1. 此处的 URL 的模式是任意的,只要视图正确即可。
  2. 默认情况下,obtain_auth_token视图明确使用 JSON 格式的请求和响应,而不是 settings 中使用默认渲染器和解析器类。
  3. 默认情况下,obtain_auth_token视图没有应用权限或限流机制。如果希望应用限流机制,则需要覆盖视图类,并使用throttle_classes属性包含它们。
  4. 如果需要自定义obtain_auth_token视图,则可以通过子类化ObtainAuthToken视图类来实现。举个例子🌰:返回token值和其他用户信息:
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response

class CustomAuthToken(ObtainAuthToken):
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data,
                                           context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({
            'token': token.key,
            'user_id': user.pk,
            'email': user.email
        })

3,在管理后台
也在管理后台手动创建令牌。
但如果使用的是大型用户群,官方建议修改TokenAdmin类,或者更具体的做法就是将TokenAdmin类的user字段声明为raw_field。举个例子🌰:

from rest_framework.authtoken.admin import TokenAdmin

TokenAdmin.raw_id_fields = ['user']

4,使用 Django manage.py 命令
从 DRF 3.6.4 版开始,可以使用以下命令手动生成用户令牌:

./manage.py drf_create_token <username>
  • 此命令将以 API 形式返回给定用户的token,如果不存在则创建它:
Generated token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b for user user1

如果想重新生成令牌(举个例子🌰:如果token已被破坏或泄露),可以传递 -r 参数:

./manage.py drf_create_token -r <username>

3,SessionAuthentication

使用 Django 默认的 session 后端进行身份验证。

  • SessionAuthentication适用于与你的网站在相同的Session环境中运行的 AJAX 客户端。

如果成功验证,SessionAuthentication 提供以下信息:

  • request.user 是一个 Django User 实例
  • request.auth 是 None。

那些被拒绝的未经身份验证的请求会返回 HTTP 403 Forbidden 响应。

如果正在使用带有 SessionAuthentication 的 AJAX 样式的 API,则应确保任何“不安全”的HTTP方法调用(如:PUT, PATCH, POST or DELETE请求)都包含有效的CSRF token,请参阅Django CSRF 文档

  • 在创建登陆页面时,始终要使用Django的标准登陆视图以确保你的登陆视图被正确的认证所保护。

4,RemoteUserAuthentication

RemoteUserAuthentication允许将身份验证委托给REMOTE_USER 配置的 Web 服务器。

如果成功验证,RemoteUserAuthentication 提供以下信息:

  • request.user 是一个 Django User 实例
  • request.auth 是 None。

(五)自定义身份验证

要实现自定义的认证方案,要继承BaseAuthentication类并且重写.authenticate(self, request) 方法。

如果认证成功,该方法应返回(user, auth)的二元元组,否则返回 None。

在某些情况下,你可能不想返回 None,而是希望从.authenticate()方法抛出 AuthenticationFailed 异常,应该采取的方法是:

  • 如果不尝试验证,返回 None。还将检查任何其他正在使用的身份验证方案。
  • 如果尝试验证但失败,则抛出 AuthenticationFailed 异常。无论任何权限检查也不检查任何其他身份验证方案,立即返回错误响应。

也可以重写.authenticate_header(self, request)方法。如果实现该方法,则应返回一个字符串,该字符串将用作 HTTP 401 Unauthorized 响应中的 WWW-Authenticate 头的值。
如果该方法未被重写,则认证方案将在未验证的请求被拒绝访问时返回 HTTP 403 Forbidden 响应。

举个例子🌰:将以自定义请求标头中名称为 X_USERNAME 提供的用户名作为用户对任何传入请求进行身份验证。

from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions

class ExampleAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        username = request.META.get('HTTP_X_USERNAME')
        if not username:
            return None

        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            raise exceptions.AuthenticationFailed('No such user')

        return (user, None)

回到教程,根据源码流程,我们需要先对请求进行身份验证然后才说权限验证的时。
尽管我们可以自定义任何身份验证类,但还是使用默认的SessionAuthenticationBasicAuthentication

五,权限

DRF 的权限实现与 django 的权限实现有所不同:

  • 后者将权限抽象为一个单独的模型
  • 前者将权限抽象为一个个不存储数据的插件

(一)DRF 中的权限系统

在 DRF 中,权限系统连同身份验证系统和限流系统一起决定是否应该允许或拒绝请求对视图的访问。

  • 权限用于授予或拒绝不同类型的用户对 API 不同部分的访问:能不能访问视图,能不能处理对象。

1,如何确定权限

Rest Framework 中的权限判断依据,始终被定义为一个权限类的列表。

  • 身份验证之后就是权限检查,最后才决定是否运行视图或是处理对象。
  • 系统根据列表中每个权限类对请求中的 request.userrequest.auth 信息的情况来确定如何处理传入的请求。
  • 如果检查失败,会抛出一个exceptions.PermissionDeniedexceptions.NotAuthenticated异常,并且视图的主体将不会运行。

当权限检查失败时,将返回 “403 Forbidden” 或 “401 Unauthorized” 响应,具体根据以下规则:

  • 请求已成功通过身份验证,但权限被拒绝—— 将返回 403 Forbidden 响应。
  • 请求未成功认证,最高优先级的认证类不使用WWW-Authenticate标头——将返回 403 Forbidden 响应。
  • 请求未成功认证,最高优先级的认证类使用WWW-Authenticate标头——将返回 HTTP 401 未经授权的响应,并附带适当的WWW-Authenticate标头。

2,对象级权限

Rest Framework 还支持对象级的权限,用于确定是否允许用户操作特定对象(通常是模型实例)。

当调用.get_object()时,由 Rest Framework 的通用视图运行对象级权限检测。

  • 与视图级别权限一样,如果不允许用户操作给定对象,则会抛出exceptions.PermissionDenied异常。

如果你正在编写自己的视图并希望强制执行对象级权限检测,或者你想在通用视图中重写get_object方法,那么你需要在检索对象的时候显式调用视图上的.check_object_permissions(request, obj)方法。

  • 这将引发一个PermissionDeniedNotAuthenticated异常,或者如果视图有适当的权限就简单地返回。举个例子🌰:
def get_object(self):
    obj = get_object_or_404(self.get_queryset(), pk=self.kwargs["pk"])
    self.check_object_permissions(self.request, obj)
    return obj

出于性能原因,通用视图在返回对象列表时不会自动将对象级别权限应用到查询集中的每个实例。

通常,当使用对象级别的权限时,还会希望适当地过滤 queryset,以确保用户只对他们允许查看的实例具有可见性。

因为在创建对象时没有调用get_object()方法,所以也就不会应用来自has_object_permission()方法的对象级别权限。

为此,我们将需要创建一个自定义权限。

  • 为了限制对象的创建,需要在 Serializer 类中实现权限检查,或者重写 ViewSet
    类的perform_create()方法。

3,设置权限策略

最简单的权限策略是允许任何经过身份验证的用户对视图的访问,拒绝任何未经身份验证的用户对视图的访问。

  • 这对应于 Rest Framework 中的 IsAuthenticated 类。

稍微不那么严格的权限策略是允许对经过身份验证的用户进行完全访问,但允许对未经过身份验证的用户进行只读访问。

  • 这对应于 Rest Framework 中的 IsAuthenticatedOrReadOnly 类。

Rest Framework 提供了多个权限配置类以供配置权限策略。

默认权限策略可以使用DEFAULT_PERMISSION_CLASSES进行全局设置。举个例子🌰:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}
  • 如果未指定,则此设置默认为允许无限制访问:
'DEFAULT_PERMISSION_CLASSES': (
   'rest_framework.permissions.AllowAny',
)

还可以使用基于APIView类的视图在每个视图或每个视图集的基础上设置身份验证策略。举个例子🌰:

from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
    permission_classes = (IsAuthenticated,)

    def get(self, request, format=None):
        content = {
            'status': 'request was permitted'
        }
        return Response(content)

也可以使用@api_view装饰器来装饰函数视图。举个例子🌰:

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

总之,无论通过类属性或装饰器配置权限策略,都会忽略配置文件中默认的权限策略(除非这个 API 视图没有单独配置权限策略)。

(二)可用的权限控制类

1,AllowAny

AllowAny 权限类将允许不受限制的访问,无论请求是经过身份验证还是未经身份验证

此权限不是严格要求的,因为可以通过为权限配置使用空列表或元组来获得相同的结果,但直接指定此类确实很有用,因为它使不受限制的访问的意图更加明确。

2,IsAuthenticated

IsAuthenticated 权限类将拒绝任何未经身份验证的用户的权限,否则允许权限。

如果希望 API 仅供注册用户访问,则此权限适用。

3,IsAdminUser

IsAdminUser 权限类将拒绝任何普通用户的权限,除非该用户是超级用户。

如果 API 仅可供受信任的管理员访问,则此权限适用。

4,IsAuthenticatedOrReadOnly

IsAuthenticatedOrReadOnly 类允许经过身份验证的用户执行任何请求。只有当请求方法是“安全”方法(GET, HEAD 或 OPTIONS)之一时,才允许未经授权的用户的请求。

如果 API 允许匿名用户具有读取权限,并且只允许经过身份验证的用户具有写入权限,则此权限适用。

这就是我们需要的:只需要将它添加到 SnippetList 和 SnippetDetail 视图类中,就意味着只有经过身份验证的请求获得读写访问权限。看看代码内容👨‍💻:

class SnippetList(mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  generics.GenericAPIView):
    ...
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

class SnippetDetail(mixins.RetrieveModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.DestroyModelMixin,
                    generics.GenericAPIView):
    ...
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

5,DjangoModelPermissions

DjangoModelPermissions 类与 Django 的标准模型权限相关联,只能应用于具有.queryset属性或get_queryset()方法的类视图。仅当用户通过身份验证并分配了相关模型权限时,才会授予授权。

  • POST 请求要求用户拥有 add 模型的权限。
  • PUT 、PATCH 请求要求用户拥有 change 模型的权限。
  • DELETE 请求要求用户拥有 delete 模型的权限。

默认行为也可以被重写以支持自定义模型权限。例如,你可能希望为 GET 请求授予查看模型的权限。

  • 要使用自定义模型权限,请覆盖DjangoModelPermissions并设置.perms_map属性。有关详细信息,请参阅源代码:
class DjangoModelPermissions(BasePermission):
    """
    该请求使用django.contrib.auth 权限。请参阅: https  ://docs.djangoproject.com/en/dev/topics/auth/#permissions
    
	它确保用户经过身份验证,并且对模型具有适当的添加/更改/删除权限。

	此权限只能应用于提供.queryset属性的视图类。
    """

	# 将方法映射到所需的权限代码。如果还需要提供“查看”权限,或者如果想提供自定义权限代码,请覆盖此选项。
    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'],
    }

    authenticated_users_only = True

    def get_required_permissions(self, method, model_cls):
        """
        给定一个模型和一个 HTTP 方法,返回用户需要拥有的权限代码列表。
        """
        kwargs = {
            'app_label': model_cls._meta.app_label,
            'model_name': model_cls._meta.model_name
        }

        if method not in self.perms_map:
            raise exceptions.MethodNotAllowed(method)

        return [perm % kwargs for perm in self.perms_map[method]]

    def _queryset(self, view):
        assert hasattr(view, 'get_queryset') \
            or getattr(view, 'queryset', None) is not None, (
            'Cannot apply {} on a view that does not set '
            '`.queryset` or have a `.get_queryset()` method.'
        ).format(self.__class__.__name__)

        if hasattr(view, 'get_queryset'):
            queryset = view.get_queryset()
            assert queryset is not None, (
                '{}.get_queryset() returned None'.format(view.__class__.__name__)
            )
            return queryset
        return view.queryset

    def has_permission(self, request, view):
        # 确保使用 DefaultRouter 时不将 DjangoModelPermissions 应用于根视图的解决方法。
        if getattr(view, '_ignore_model_permissions', False):
            return True

        if not request.user or (
           not request.user.is_authenticated and self.authenticated_users_only):
            return False

        queryset = self._queryset(view)
        perms = self.get_required_permissions(request.method, queryset.model)

        return request.user.has_perms(perms)

6,DjangoModelPermissionsOrAnonReadOnly

DjangoModelPermissions类似,但也允许未经身份验证的用户具有对API的只读访问权限。

7,DjangoObjectPermissions

DjangoObjectPermissions类与Django的标准对象权限框架相关联,该框架允许模型上的每个对象的权限。

  • 为了使用此权限类,你还需要添加支持对象级权限的权限后端,例如django-guardian

DjangoModelPermissions一样,此权限只能应用于具有.queryset属性或.get_queryset()方法的视图。只有在用户通过身份验证并且具有相关的每个对象权限和相关的模型权限后,才会被授予权限。

  • POST 请求要求用户对模型实例具有添加权限。
  • PUT 和PATCH 请求要求用户对模型实例具有更改权限。
  • DELETE请求要求用户对模型实例具有删除权限。 请注意,DjangoObjectPermissions 不需要
    django-guardian软件包,并且应当同样支持其他对象级别的后端。

DjangoModelPermissions一样,你可以通过重写DjangoObjectPermissions并设置.perms_map属性来使用自定义模型权限。有关详细信息,请参阅源代码。

(三)自定义权限控制类

要实现自定义权限,请重写BasePermission并实现以下方法中的一个或全部:

  • .has_permission(self, request, view)
  • .has_object_permission(self, request, view, obj)

如果请求被授予访问权限,方法应该返回 True,否则返回 False。

如果要测试请求是读取操作还是写入操作,则应该根据常量SAFE_METHODS检查请求方法。举个例子🌰:

  • 常量SAFE_METHODS是包含’GET’, ‘OPTIONS’和’HEAD’的元组。
if request.method in permissions.SAFE_METHODS:
    # 检查只读请求的权限
else:
    # 检查读取请求的权限

如果校验失败,自定义权限将引发 PermissionDenied 异常。要更改与异常关联的错误消息,请直接在自定义权限上实现 message 属性。否则将使用PermissionDenieddefault_detail属性。举个例子🌰:

from rest_framework import permissions

class CustomerAccessPermission(permissions.BasePermission):
    message = 'Adding customers not allowed.'

    def has_permission(self, request, view):
         ...
  • 仅当视图级has_permission检查已通过时,才会调用实例级has_object_permission方法。
  • 为了运行实例级别检查,视图代码应显式调用.check_object_permissions(request, obj)。如果你使用的是通用视图,那么默认会为你处理。

举个例子🌰:根据黑名单检查传入请求的IP地址的权限类的示例:如果IP已被列入黑名单,则拒绝该请求。

from rest_framework import permissions

class BlacklistPermission(permissions.BasePermission):
    """
    对列入黑名单的IP进行全局权限检查。
    """

    def has_permission(self, request, view):
        ip_addr = request.META['REMOTE_ADDR']
        blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists()
        return not blacklisted

除了针对所有传入请求运行的全局权限外,还可以创建对象级权限,这些权限仅针对影响特定对象实例的操作运行。举个例子🌰:

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    对象级权限仅允许对象的所有者对其进行编辑
    假设模型实例具有`owner`属性。
    """

    def has_object_permission(self, request, view, obj):
        # 任何请求都允许读取权限,
        # 所以我们总是允许GET,HEAD或OPTIONS 请求.
        if request.method in permissions.SAFE_METHODS:
            return True

        # 示例必须要有一个名为`owner`的属性
        return obj.owner == request.user

如果 request.method 属性的 HTTP 动词不是三种安全方法中的任何一种,则代码返回 True 。仅在接收到的 obj 对象的所有者属性 (obj.owner) 与发起请求的用户匹配时授予写权限 ( 请求用户)。 发起请求的用户将始终是经过身份验证的用户。 这样,只有相关资源的所有者才会被授予那些包含不安全 HTTP 动词的请求的写权限。

回到教程中,所有的代码片段(也就是 Snippet 对象的每个实例)已经都可以被任何登录的用户看到,但希望只有创建代码片段的用户才能更新或删除它。此时我们可以继续向权限策略中添加权限类。但由于没有像成合适的权限类,所以要自定义一个。

在 snippets app 中,创建一个新文件permissions.py。看看代码内容👨‍💻:

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

然后在 SnippetDetail 视图中编辑 permission_classes 属性将该自定义权限添加到我们的代码片段实例路径。看看代码内容👨‍💻:

class SnippetDetail(mixins.RetrieveModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.DestroyModelMixin,
                    generics.GenericAPIView):
    ...
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

如果再次打开浏览器,你会发现如果你以代码片段创建者的身份登录的话,DELETE 和 PUT 操作才会显示在代码片段实例路径上。

测试一下:

http POST http://127.0.0.1:8000/snippets/ code="print 123"

{
    "detail": "Authentication credentials were not provided."
}
http -a tom:password123 POST http://127.0.0.1:8000/snippets/ code="print 789"

{
    "id": 5,
    "owner": "tom",
    "title": "foo",
    "code": "print 789",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

或者打开网页访问各个 API 进行测试:
1,http://127.0.0.1:8000/snippets/
在这里插入图片描述
2,http://127.0.0.1:8000/snippets/1/
在这里插入图片描述
3,http://127.0.0.1:8000/snippets/4/
在这里插入图片描述
4,http://127.0.0.1:8000/users/
在这里插入图片描述
5,http://127.0.0.1:8000/users/2/
在这里插入图片描述
5,http://127.0.0.1:8000/api-auth/
在这里插入图片描述

六,限流

权限是让用户的访问能力受限,限流就是让访问频率受限。
比如说后端提供了一个对收费资源的访问服务,要求每个用户每天只能使用十次这个服务,甚至是为相关的存储服务限制带宽,这都是限流。

有多种方式在 django 中实现限流,最常用的就是使用中间件,无论是完全地自定义中间件,还是重写 MiddlewareMixin

Rest Framework 也提供限流机制。

(一)如何确定限流

Rest Framework 的限流机制与权限机制类似,它的限流判断依据,始终被定义为一个限流类的列表。它会在运行视图主体之前,检查列表中的每一个限流器。如果任何限流器检查异常,则将抛出exceptions.Throttled,并且视图主体将不会运行。

(二)限流策略

可以使用DEFAULT_THROTTLE_CLASSES和DEFAULT_THROTTLE_RATES设置全局设置默认的限流策略。举个例子🌰:

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [	# 指定限流器
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {		# 指定限流周期
        'anon': '100/day',
        'user': '1000/day'
    }
}
  • 用于 DEFAULT_THROTTLE_RATES 限流周期的流量描述可以包括 second, minute, hour 或 day 。

对于基于 APIView 类的视图,可以为每个视图或每个视图集设置单独的限流策略。举个例子🌰:

from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView

class ExampleView(APIView):
    throttle_classes = (UserRateThrottle,)

    def get(self, request, format=None):
        content = {
            'status': 'request was permitted'
        }
        return Response(content)

对于使用 @api_view 装饰器的函数视图也可设置。举个例子🌰:

@api_view(['GET'])
@throttle_classes([UserRateThrottle])
def example_view(request, format=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

也可以为使用@action装饰器创建的路由设置限流策略。举个例子🌰

  • 以这种方式设置的限流策略将覆盖任何视图集级别的设置。
@action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle])
def example_adhoc_method(request, pk=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

(三)如何识别客户端

X-Forwarded-For HTTP 请求头和 REMOTE_ADDR WSGI 变量用于唯一地标识用户的 IP 地址。

  • 后者会在使用了 X-Forwarded-For 请求头后被替代。

如果需要严格标识唯一的客户端 IP 地址,则需要首先通过设置 NUM_proxies 来配置 API 后运行的应用程序的代理数量。此设置应为零或更大的整数。

  • 如果设置为非零,那么一旦第一次排除任何应用程序代理 IP 地址,客户端 IP 将被标识为 X-Forwarded-For 标头中的最后一个 IP 地址。
  • 如果设置为零,则 Remote-Addr 头将始终用于标识 IP 地址。

重要的是要了解,如果设置了 NUM_PROXIES,那么一个唯一的 NAT’d 网关后面的所有客户端都将被视为单个客户端。

关于 X-Forwarded-For 请求头如何工作以及标识远程客户端 IP 的更多上下文可以参考这里

(四)设置缓存

REST framework 提供的限流类使用 Django 的缓存后端。使用时应该首先确保已设置适当的缓存配置

  • 对于简单的限流配置,后端默认值LocMemCache应该没问题。有关更多详细信息,请参阅 Django 的缓存文档
  • 如果需要使用默认配置以外的缓存,则可以通过创建自定义限流类并设置cache属性来实现。举个例子🌰:
from django.core.cache import caches

class CustomAnonRateThrottle(AnonRateThrottle):
    cache = caches['alternate']

(五)可用的限流控制类

1,AnonRateThrottle

AnonRateThrottle依据根据传入请求的 IP 地址生成的唯一密钥来限流未经身份验证的用户。

允许的请求率由以下之一确定(按优先顺序):

  • 覆盖AnonRateThrottle,并设置类视图的 rate 属性。
  • 使用DEFAULT_THROTTLE_RATES['anon']中的设置。

如果要限制来自未知源的请求速率,则 AnonRateThrottle 是合适的。

2,UserRateThrottle

UserRateThrottle 将根据传入请求的用户 ID 生成的唯一密钥来按给定的请求速率进行限流。未经身份验证的请求将使用AnonRateThrottle

允许的请求速率由以下之一确定(按优先顺序):

  • 覆盖UserRateThrottle,并设置类视图的 rate 属性。
  • 使用DEFAULT_THROTTLE_RATES['user']中的设置。

一个 API 还可以同时具有多个 UserRateThrottles。为此需重写 UserRateThrottle 并为每个类设置一个唯一的作用范围。举个例子🌰:实现多个用户限速:

class BurstRateThrottle(UserRateThrottle):
    scope = 'burst'

class SustainedRateThrottle(UserRateThrottle):
    scope = 'sustained'

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'example.throttles.BurstRateThrottle',
        'example.throttles.SustainedRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'burst': '60/min',
        'sustained': '1000/day'
    }
}

如果想要为每个用户实现简单的全局速率限制,则UserRateThrottle是合适的。

3,ScopedRateThrottle

ScopedRateThrottle 根据将请求的作用范围与唯一的用户 ID 或 IP 地址关联起来所形成唯一的密钥来限制对 API 特定部分的访问。

  • 仅当所访问的视图包含 throttle_scope 属性时,才会应用此限流器。
  • 允许的请求速率由 DEFAULT_THROTTLE_RATES中的作用范围配置所确定。

举个例子🌰:用户请求 ContactListView 或 ContactDetailView 将被限制为每天总共 1000 次。用户请求 UploadView 将被限制为每天 20 次:

class ContactListView(APIView):
    throttle_scope = 'contacts'
    ...

class ContactDetailView(APIView):
    throttle_scope = 'contacts'
    ...

class UploadView(APIView):
    throttle_scope = 'uploads'
    ...


REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.ScopedRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'contacts': '1000/day',
        'uploads': '20/day'
    }
}

(六)自定义限流

若要创建自定义限流器,则需要重写 BaseThrottle 并使用 .allow_request(self, request, view)

  • 如果允许请求,则该方法应返回 True ,否则返回 False。

也可以选择性地重写 .wait()方法,它应返回尝试下一个请求前建议的等待秒数,或者 None。

  • 只有在 .allow_request() 先前已返回 False 时才会调用 .wait()方法。
  • 响应中将包含 Retry-After 头。

举个例子🌰:在每10个请求中随机地限制1个:

import random

class RandomRateThrottle(throttling.BaseThrottle):
    def allow_request(self, request, view):
        return random.randint(1, 10) != 1

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/98058.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!