使用Django构建REST API - 测试驱动的方法:第2部分

2017-09-01

使用Django构建REST API - 测试驱动的方法:第2部分

The precondition to freedom is security – Rand Beers

验证是API安全性的关键部分。

但首先,一些回顾。

在本系列的第1部分中,我们了解了如何使用TDD方法创建bucketlist API。我们涵盖了Django中的写作测试,并且也了解了Django Rest框架。

我们将在本系列的第2部分中介绍补充主题。在大多数情况下,我们将深入了解Django驱动的bucketlist API中的身份验证和授权用户。如果您还没有检查第1部分,现在是开始粉碎之前这样做的机会。

好吧回到业务!

验证与授权

认证通常与授权混淆。他们不一样。

您可以将认证视为验证某人身份的方式。(用户名,密码,令牌,密钥等)和授权作为确定被授权用户的访问级别的方法。

当我们查看我们的bucketlist API时,它大部分是有效的。然而,它缺乏能力,例如知道谁创建了一个桶列表,一个给定的用户是否首先进行身份验证,甚至是否有权对桶列表进行更改。

我们需要解决这个问题。

我们首先实施身份验证,然后删除一些授权功能。


实现

可以在DRF API中实现身份验证。起点很简单 - 您首先要跟踪用户。

那么我们如何实现呢?Django提供了一个我们可以玩的默认用户模型。

好。让我们完成

我们将在Bucketlist模型上创建一个所有者字段。这就是为什么:用户可以创建一个bucketlist - 这意味着一个bucketlist有一个owner。因此,我们将在我们的bucketlist模型中添加一个用户的字段定义。

# rest_api/models.py

from django.db import models

class Bucketlist(models.Model):
    """This class represents the bucketlist model."""
    name = models.CharField(max_length=255, blank=False, unique=True)
    owner = models.ForeignKey('auth.User',  # ADD THIS FIELD
    related_name='bucketlists', 
    on_delete=models.CASCADE) 
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    def __str__(self):
        """Return a human readable representation of the model instance."""
        return "{}".format(self.name)

所有者fieild使用ForeignKey接受许多参数的类。第一个auth.User只是指向我们希望建立关系的模型类。

外键将来自模型类,auth.User以实现用户和Bucketlist模型之间的关系。

完成之后,我们必须运行我们的migration,以反映数据库中的模型更改。

运行:

$ python3  manage.py  makemigrations  rest_api

一点要注意:

在现有表格上写入新字段时,您可能会遇到这样的情况:

mirgate

数据库警告,我们正在尝试添加一个不可为空的字段,该字段不应为null或缺少值。我们需要一个值,因为我们在数据库上有预先存在的数据。在开发环境下的一个简单的攻击将是删除migrations应用程序和db.sqlite3文件中的文件夹。这将摆脱我们最后创建的桶列表。我们可以随时创建一个新的。但是,您不应该在生产环境中执行此操作,因为您将丢失所有数据库数据。更清洁的方法来解决它是提供一次性的默认值。但是如果您的数据库中没有记录,请随时删除修订。

执行此操作后,我们将使用migrate命令将更改提交给我们的数据库:

$ python3 manage.py migrate

重构我们的测试

到目前为止,我们还没有编写任何适用于新用户认证的测试。因此,我们必须重构现有的测试用例。

但首先,我们必须知道要写的内容。

我们来做一些分析。我们需要考虑的变化是:

  • Bucketlist用户所有权 - 指向集成默认的Django用户模型
  • 确保通过验证的用户进行请求 - 这意味着我们将在发送HTTP请求之前强制执行身份验证
  • 将桶列表创建限制为仅验证用户
  • 限制仅由其所有者访问的现有桶单
  • 这些要点将会引导我们重构我们的测试。

重构ModelTestCase

我们将导入默认的用户模型(django.contrib.auth.User)到我们的测试模块中以创建一个用户。

# rest_api/tests.py
from django.contrib.auth.models import User

用户将帮助我们测试桶列表的所有者。我们将在我们的setUp方法中创建用户,这样我们就不必在每次使用它时创建它。

class ModelTestCase(TestCase):
    """This class defines the test suite for the bucketlist model."""

    def setUp(self):
        """Define the test client and other test variables."""
        user = User.objects.create(username="nerd") # ADD THIS LINE
     self.name = "Write world class code"
        # specify owner of a bucketlist
        self.bucketlist = Bucketlist(name=self.name, owner=user) # EDIT THIS TOO

在安装方法中,我们刚刚通过创建一个用户名的用户定义了一个测试用户。然后,我们已将用户的实例添加到bucketlist类中。用户现在是该桶列表的所有者。

重构ViewsTestCase

由于观点主要涉及请求,我们将确保只有身份验证和授权的用户才能访问bucketlist API。

我们为它编写一些代码

# rest_api/tests.py
# import fall here

# Model Test Case is here

class ViewTestCase(TestCase):
    """Test suite for the api views."""

    def setUp(self):
        """Define the test client and other test variables."""
        user = User.objects.create(username="nerd")

        # Initialize client and force it to use authentication
        self.client = APIClient()
        self.client.force_authenticate(user=user)

        # Since user model instance is not serializable, use its Id/PK
        self.bucketlist_data = {'name': 'Go to Ibiza', 'owner': user.id}
        self.response = self.client.post(
            reverse('create'),
            self.bucketlist_data,
            format="json")

    def test_api_can_create_a_bucketlist(self):
        """Test the api has bucket creation capability."""
        self.assertEqual(self.response.status_code, status.HTTP_201_CREATED)

    def test_authorization_is_enforced(self):
        """Test that the api has user authorization."""
        new_client = APIClient()
        res = new_client.get('/bucketlists/', kwargs={'pk': 3}, format="json")
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_api_can_get_a_bucketlist(self):
        """Test the api can get a given bucketlist."""
        bucketlist = Bucketlist.objects.get(id=1)
        response = self.client.get(
            '/bucketlists/',
            kwargs={'pk': bucketlist.id}, format="json")

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertContains(response, bucketlist)

    def test_api_can_update_bucketlist(self):
        """Test the api can update a given bucketlist."""
        bucketlist = Bucketlist.objects.get()
        change_bucketlist = {'name': 'Something new'}
        res = self.client.put(
            reverse('details', kwargs={'pk': bucketlist.id}),
            change_bucketlist, format='json'
        )
        self.assertEqual(res.status_code, status.HTTP_200_OK)

    def test_api_can_delete_bucketlist(self):
        """Test the api can delete a bucketlist."""
        bucketlist = Bucketlist.objects.get()
        response = self.client.delete(
            reverse('details', kwargs={'pk': bucketlist.id}),
            format='json',
            follow=True)
        self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT)

我们初始化了ApiClient并强制它使用身份验证。这将强化API的安全性。桶列表所有权也被考虑在内。另外,请注意我们如何self.client在每个测试方法中始终使用,而不是创建新的测试方法 这是为了确保我们重新使用经过身份验证的客户端。可重用性是好的做法。:-)很棒!

运行测试 他们现在应该失败。

python3  manage.py  test  rest_api

下一步是重构我们的代码,使这些失败的测试通过。

如何通过这些测试!

首先,集成用户

在大多数情况下,您在模型中所做的任何更改也应反映在序列化程序中。这是因为序列化器直接与模型接口,这有助于将奇怪的查询集更改为json,反之亦然。

我们来编辑我们的bucketlist serializer。我们将简单地跳入serializers.py文件并编写一个我们最好调用的自定义字段owner。这是一个桶单的所有者。

# rest_api/serializers.py

class BucketlistSerializer(serializers.ModelSerializer):
    """Serializer to map the model instance into json format."""

    owner = serializers.ReadOnlyField(source='owner.username') # ADD THIS LINE

    class Meta:
        """Map this serializer to a model and their fields."""
        model = Bucketlist
        fields = ('id', 'name', 'owner', 'date_created', 'date_modified') # ADD 'owner'
        read_only_fields = ('date_created', 'date_modified')

所有者字段是只读的,以便使用我们的api的用户不能更改桶列表的所有者。不要忘记按照上面的说明将所有者添加到字段中。

我们来运行它,看看它是否有效:启动服务器 python3 manage.py runserver

当我们从localhost访问它时,我们应该看到这样的页面:

localhost

现在,当创建一个新的桶单时,我们需要一个方法来保存所有者。保存桶列表是在我们定义的名为CreateView的类中完成的views.py。我们将通过添加一个**perform_create(self, serializer)**方法来编辑我们的CreateView类。这种方法使我们能够控制如何保存序列化程序。

# rest_api/views.py

# We are inside the CreateView class
...

    def perform_create(self, serializer):
            """Save the post data when creating a new bucketlist."""
            serializer.save(owner=self.request.user) # Add owner=self.request.user

在**serializer.save()**接受现场参数。在这里,我们指定了owner参数。为什么?因为我们的串行器具有一个字段 - 这意味着我们可以在一个序列化程序的保存方法中指定所有者,然后将用户作为其所有者保存桶列表。

当我们尝试创建一个bucketlist

error page 500

DB 时,我们现在应该会看到一个这样的错误 。为什么值错误?很好的问题 - 这只是因为我们试图从浏览器中保存一个bucketlist 而不指定所有者!

我们的新的不可为空的所有者字段在序列化程序可以验证和保存桶单之前需要一个值。

我们马上解决这个问题。

在我们这里urls.py,我们将添加一条路线,帮助用户在创建一个桶单之前登录到我们的api。我们这样做是为了允许一个存储区列表拥有所有者,也就是说,如果登录用户决定创建一个。

# rest_api/urls.py
# imports fall here

urlpatterns = {
    url(r'^auth/', include('rest_framework.urls', # ADD THIS URL
                               namespace='rest_framework')), 
    url(r'^bucketlists/$', CreateView.as_view(), name="create"),
    url(r'^bucketlists/(?P<pk>[0-9]+)/$',
        DetailsView.as_view(), name="details"),
}

urlpatterns = format_suffix_patterns(urlpatterns)

这个新行包括提供默认登录模板以验证用户的DRF路由。你可以拨打任何你想要的路线auth。

保存文件。它将自动刷新正在运行的服务器实例。

当您访问时,您现在应该会看到屏幕右上方的登录按钮

http://localhost/bucketlists/

点击按钮将重定向到登录模板。

login page

我们创建一个超级用户登录。

$ python3 manage.py createsuperuser

使用我们刚才指定的用户名和密码登录应该是轻而易举的。

授权:添加权限

现在,任何用户都可以查看和编辑任何bucketlist。我们希望将用户绑定到其列表中,以便只有所有者可以对编辑和删除进行更改。

默认权限检查

我们可以使用默认权限包来限制仅对经过身份验证的用户的桶列表访问。

views.py中,我们将导入权限类

from rest_framework import permissions

然后在我们的CreateView类中,我们将添加权限类IsAuthenticated

# rest_api/views.py

class CreateView(generics.ListCreateAPIView):
    """This class handles the GET and POSt requests of our rest api."""
    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer
    permission_classes = (permissions.IsAuthenticated,) # ADD THIS LINE

权限类IsAuthenticated将拒绝任何未经身份验证的用户的权限,否则允许其他权限。我们可以用IsAuthenticatedOrReadOnly它允许未经授权的用户,如果请求是“安全”的方法之一(GETHEADOPTIONS)。但是我们要保证安全 - 我们会坚持下去IsAuthenticated

自定义权限

现在,任何经过身份验证的用户都可以看到其他用户的分类列表。要实现所有权的全部概念,我们必须创建一个自定义权限。

让我们创建一个名为permissions.py里面rest_api的目录。在这个文件里面,我们写下面的代码:

from rest_framework.permissions import BasePermission
from .models import Bucketlist

class IsOwner(BasePermission):
    """Custom permission class to allow only bucketlist owners to edit them."""

    def has_object_permission(self, request, view, obj):
        """Return True if permission is granted to the bucketlist owner."""
        if isinstance(obj, Bucketlist):
            return obj.owner == request.user
        return obj.owner == request.user

上面的类实现了这个真相所持有的权限 - 用户必须是拥有该对象许可的所有者。如果他们确实是该桶列表的所有者,则它返回True,否则返回False

我们只需将其添加到我们的permission_classes元组中即可。为了清楚起见,view.py现在更新的内容应如下所示:

# rest_api/views.py

from rest_framework import generics, permissions
from .permissions import IsOwner
from .serializers import BucketlistSerializer
from .models import Bucketlist

class CreateView(generics.ListCreateAPIView):
    """This class handles the GET and POSt requests of our rest api."""
    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer
    permission_classes = (
        permissions.IsAuthenticated, IsOwner)

    def perform_create(self, serializer):
        """Save the post data when creating a new bucketlist."""
        serializer.save(owner=self.request.user)

class DetailsView(generics.RetrieveUpdateDestroyAPIView):
    """This class handles GET, PUT, PATCH and DELETE requests."""

    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer
    permission_classes = (
        permissions.IsAuthenticated,
        IsOwner)

如果我们注销并尝试获取分类列表,我们将受到HTTP 403 Forbidden响应。这意味着我们的认证和授权实际上正在运作!

真棒!

http 403 forbidden

最后,我们运行我们的测试,看看他们是否会通过:

$ python3 manage.py test

running test

快速移动

基于令牌的身份验证如何?

令牌认证适用于客户端 - 服务器设置,特别是消费客户端是本机桌面或本地移动设备时。

这是如何工作的 - 用户从服务器请求安全令牌。服务器生成令牌并将其与该用户关联。发送令牌后,服务器等待用户使用该特定令牌请求资源。然后,用户可以使用该令牌对服务器进行身份验证并证明他/她确实是有效的用户。

对于我们在API中使用令牌认证,我们必须在settings.py文件上设置一些配置。

我们添加rest_framework.authtoken我们的已安装应用程序列表,如下所示:

# project/settings.py

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_api', # note the comma (if you lack it, errors! the horror!)
    'rest_framework.authtoken' # ADD THIS LINE
)

每次我们创建一个用户,我们也想为他们创建安全令牌。但是我们如何确保用户创建也会触发令牌创建?

输入信号。

Django封装了一个信号调度程序。调度器就像发送通知他人一个刚刚发生的事件的messanger。当用户创建时,post_save用户模型将发出一个信号。一个接收器(这是一个简单的功能),然后将帮助我们抓住这个post_save信号,并立即创建令牌。

我们的接收器将存在我们的models.py文件中。将 the post_save signalthe default User modelthe Token modelthe receiver 导入到 model.py 文件中

from django.db.models.signals import post_save
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
from django.dispatch import receiver

然后在文件的底部写接收器,如下所示:

# rest_api/models.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
from django.dispatch import receiver

class Bucketlist(models.Model):
    """This class represents the bucketlist model."""
    name = models.CharField(max_length=255, blank=False, unique=True)
    owner = models.ForeignKey(
        'auth.User',
        related_name='bucketlists',
        on_delete=models.CASCADE)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    def __str__(self):
        """Return a human readable representation of the model instance."""
        return "{}".format(self.name)

# This receiver handles token creation immediately a new user is created.
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

请注意:

Bucketlist模型类中的接收器不缩进。在class里缩进它是一个常见的错误。

我们还需要为用户提供一种获取令牌的方式。网址将用于此目的。在urls.py上编写以下代码行:

# rest_api/urls.py
from rest_framework.authtoken.views import obtain_auth_token # add this import

urlpatterns = {
    url(r'^bucketlists/$', CreateView.as_view(), name="create"),
    url(r'^bucketlists/(?P<pk>[0-9]+)/$',
        DetailsView.as_view(), name="details"),
    url(r'^auth/', include('rest_framework.urls',
                           namespace='rest_framework')),
    url(r'^users/$', UserView.as_view(), name="users"),
    url(r'users/(?P<pk>[0-9]+)/$',
        UserDetailsView.as_view(), name="user_details"),
    url(r'^get-token/', obtain_auth_token), # Add this line
}

urlpatterns = format_suffix_patterns(urlpatterns)

其他框架非常强大,它提供了一个内置的视图,用于在用户发布用户名和密码时处理获取令牌。

我们将继续进行 migrations,并将更改的 migration 写到数据库,以便我们的应用程序可以利用此内置视图的强大功能。

$ python3 manage.py makemigrations &&  python3 manage.py migrate

最后,我们向设置添加一些配置,以便我们的应用程序可以使用BasicAuthentication和TokenAuthentication进行身份验证。

# project/settings.py

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    )
}

DEFAULT_AUTHENTICATION_CLASSES配置告诉应用程序,我们希望配置多种验证用户的方式。我们通过引用此元组内的内置身份验证类来指定方法。

运行

一旦保存,服务器将自动重新启动添加的更改,如果它已经在运行。但是,只需重新运行服务器即可python3 manage.py runserver

为了直观地测试我们的api是否仍然存在,我们将在Postman上进行HTTP请求

Postman 步骤一:获得该令牌

对于客户端进行身份验证,所获得的令牌应包含在AuthorizationHTTP头中。我们前面加上Token一个空格字符。标题应如下所示:

Authorization: Token 2777b09199c62bcf9418ad846dd0e4bbdfc6ee4b

不要忘记将空间放在两者之间。

我们将发出一个发布请求**http://localhost:8000/get-token/**,在此过程中指定用户名和密码。

postman

Postman 步骤二:在授权标题中使用获得的令牌

对于后续请求,如果我们想要访问API资源,我们必须包括授权头。

在这里可能导致错误的常见错误是为Authorization标头输入错误的格式。以下是来自服务器的常见错误消息:

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

确保您输入此格式Token <your-new-token-is-here>。如果你想在标题中有一个不同的关键字,例如Bearer简单的子类TokenAuthentication并设置关键字类变量。

让我们尝试发送一个GET请求。它应该产生这样的东西:

postman

现在你可以随时随地使用安全的API

结论

如果你已经读完了,你真的很棒!

我们已经涵盖了很多!从实现用户身份验证到创建授权的自定义权限,我们已经涵盖了大多数保护Django API的内容。

我们还方便地定义了基于令牌的身份验证层,以便移动和桌面客户端可以安全地使用我们的API。但最重要的是我们重组了测试以适应变化。这是至关重要的,仍然是测试驱动开发的核心。

如果您到达自己的问题,“ 这里发生了什么? ”,我强烈建议您看看本系列的第1部分,它恰当地提供了有关构建bucketlist API的详细教程。

快乐编码!

https://scotch.io/tutorials/build-a-rest-api-with-django-a-test-driven-approach-part-2