教程 4:身份验证和权限

目前,我们的 API 没有任何限制,任何人都可以编辑或删除代码段。我们希望有一些更高级的行为来确保

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

向我们的模型添加信息

我们将在我们的 Snippet 模型类中进行一些更改。首先,让我们添加几个字段。其中一个字段将用于表示创建代码段的用户。另一个字段将用于存储代码的高亮 HTML 表示形式。

将以下两个字段添加到 models.py 中的 Snippet 模型。

owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()

我们还需要确保在保存模型时,使用 pygments 代码高亮库填充高亮字段。

我们需要一些额外的导入

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

现在,我们可以向我们的模型类添加一个 .save() 方法

def save(self, *args, **kwargs):
    """
    Use the `pygments` library to create a highlighted HTML
    representation of the code snippet.
    """
    lexer = get_lexer_by_name(self.language)
    linenos = 'table' if self.linenos else False
    options = {'title': self.title} if self.title else {}
    formatter = HtmlFormatter(style=self.style, linenos=linenos,
                              full=True, **options)
    self.highlighted = highlight(self.code, lexer, formatter)
    super().save(*args, **kwargs)

完成后,我们需要更新我们的数据库表。通常,我们会创建一个数据库迁移来执行此操作,但出于本教程的目的,我们只需删除数据库并重新开始即可。

rm -f db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate

您可能还想创建几个不同的用户,用于测试 API。执行此操作的最快速方法是使用 createsuperuser 命令。

python manage.py createsuperuser

为我们的用户模型添加端点

现在我们有一些用户可以操作了,我们最好向我们的 API 添加这些用户的表示形式。创建新序列化程序很容易。在 serializers.py 中添加

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']

由于 'snippets' 是用户模型上的反向关系,因此在使用 ModelSerializer 类时不会默认包含它,所以我们需要为它添加一个显式字段。

我们还将在 views.py 中添加一些视图。我们希望仅对用户表示形式使用只读视图,因此我们将使用 ListAPIViewRetrieveAPIView 通用基于类的视图。

from django.contrib.auth.models import User


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


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

确保也导入 UserSerializer

from snippets.serializers import UserSerializer

最后,我们需要通过从 URL 配置中引用这些视图,将这些视图添加到 API 中。将以下内容添加到 snippets/urls.py 中的模式。

path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),

将代码段与用户关联

现在,如果我们创建了一个代码段,将无法将创建该代码段的用户与代码段实例关联起来。用户不会作为序列化表示的一部分发送,而是传入请求的属性。

我们处理此问题的方法是重写片段视图上的 .perform_create() 方法,该方法允许我们修改实例保存的管理方式,并处理传入请求或请求的 URL 中隐含的任何信息。

SnippetList 视图类中,添加以下方法

def perform_create(self, serializer):
    serializer.save(owner=self.request.user)

现在,序列化器的 create() 方法将传递一个附加的 'owner' 字段,以及来自请求的经过验证的数据。

更新我们的序列化程序

现在片段与创建它们的关联,让我们更新 SnippetSerializer 以反映这一点。在 serializers.py 中将以下字段添加到序列化器定义

owner = serializers.ReadOnlyField(source='owner.username')

注意:确保还将 'owner', 添加到内部 Meta 类中的字段列表。

此字段执行一些非常有趣的操作。source 参数控制用于填充字段的属性,并且可以指向序列化实例上的任何属性。它还可以采用上面显示的点号表示法,在这种情况下,它将遍历给定属性,类似于在 Django 模板语言中使用的方式。

我们添加的字段是未键入的 ReadOnlyField 类,与其他键入字段(例如 CharFieldBooleanField 等)相反。未键入的 ReadOnlyField 始终只读,将用于序列化表示,但不会用于在反序列化时更新模型实例。我们也可以在这里使用 CharField(read_only=True)

向视图添加必需的权限

现在代码片段与用户关联,我们希望确保只有经过身份验证的用户才能创建、更新和删除代码片段。

REST 框架包含许多权限类,我们可以使用这些类来限制谁可以访问给定视图。在这种情况下,我们正在寻找的是 IsAuthenticatedOrReadOnly,它将确保经过身份验证的请求获得读写访问权限,而未经身份验证的请求获得只读访问权限。

首先在视图模块中添加以下导入

from rest_framework import permissions

然后,将以下属性添加到 SnippetListSnippetDetail 视图类两者中。

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

向可浏览 API 添加登录

如果你打开浏览器并导航到当前的可浏览 API,你会发现你不再能够创建新的代码片段。为了做到这一点,我们需要能够以用户的身份登录。

我们可以通过编辑项目级 urls.py 文件中的 URLconf 为可浏览 API 添加一个登录视图。

在文件顶部添加以下导入

from django.urls import path, include

并且,在文件末尾,添加一个模式以包含可浏览 API 的登录和注销视图。

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

模式的 'api-auth/' 部分实际上可以是你想要使用的任何 URL。

现在,如果你再次打开浏览器并刷新页面,你将在页面的右上角看到一个“登录”链接。如果你以之前创建的其中一个用户身份登录,你将能够再次创建代码片段。

创建一些代码片段后,导航到 '/users/' 端点,并注意表示中包含与每个用户关联的片段 ID 的列表,在每个用户的 'snippets' 字段中。

对象级权限

实际上,我们希望所有代码片段对所有人可见,但也要确保只有创建代码片段的用户才能更新或删除它。

要做到这一点,我们需要创建一个自定义权限。

在片段应用程序中,创建一个新文件,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 属性,将该自定义权限添加到我们的片段实例端点

permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]

确保还要导入 IsOwnerOrReadOnly 类。

from snippets.permissions import IsOwnerOrReadOnly

现在,如果你再次打开浏览器,你会发现只有在你以创建代码片段的相同用户身份登录时,“DELETE”和“PUT”操作才会出现在片段实例端点上。

使用 API 进行身份验证

由于我们现在对 API 有一组权限,因此如果我们想要编辑任何片段,我们需要对其请求进行身份验证。我们尚未设置任何 身份验证类,因此当前应用了默认值,即 SessionAuthenticationBasicAuthentication

当我们通过 Web 浏览器与 API 交互时,我们可以登录,然后浏览器会话将为请求提供所需的验证。

如果我们以编程方式与 API 交互,我们需要在每个请求中明确提供身份验证凭据。

如果我们尝试在未经身份验证的情况下创建片段,我们将收到错误

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

{
    "detail": "Authentication credentials were not provided."
}

我们可以通过包含我们之前创建的其中一个用户的用户名和密码来发出成功的请求。

http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"

{
    "id": 1,
    "owner": "admin",
    "title": "foo",
    "code": "print(789)",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

摘要

现在,我们在 Web API 上获得了一组相当细粒度的权限,以及系统用户及其创建的代码片段的端点。

在教程的 第 5 部分 中,我们将了解如何通过为我们突出显示的片段创建 HTML 端点,以及通过使用系统中关系的超链接来提高我们 API 的内聚性,将所有内容联系在一起。