Tutorial 4: 授权(Authentication)与权限(Permissions)

当前,我们的API没有限制谁能编辑或删除snippets代码。我们想要一些更高级的行为以确保:

  • snippets数据总是与创建者联系在一起。
  • 只有授权用户才能创建snippets。
  • 只有snippet的创建者才能更新或者删除它。
  • 没有授权的请求应该只有只读权限。

在我们的模型中添加信息

我们打算对我们的Snippet模型类做些改变。首先,让我们添加几个字段。其中一个字段将显示出哪个用户创建里snippet数据。另一个字段将用于HTML代码高亮。

owner = models.ForeignKey('auth.User', related_name='snippets')
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 = 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)

然后,我们需要更细我们的数据库表。为此,正常情况下,我们会创建数据库迁移(database migration),但是就本教程来说,我们只需要删除原来的数据库,然后重新创建即可。

rm -f tmp.db 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。添加以下urls.py中。

url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),

将用户和Snippets连接起来

现在,如果我们创建snippet数据,我们没办法将用户和snippet实例联系起来。虽然用户不是序列表示的部分,但是它是请求的一个属性。 我们通过重写snippet视图的.perform_create()方法来做到,这个方法允许我们修改如何保存实例,修改任何请求对象或者请求连接里的信息。 在SnippetList视图类中添加以下方法;

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

现在,我们序列的create()方法将会另外传入一个来自有效的请求数据的'owner'字段。

更新我们的序列

既然已经将snippets和创建它们的用户联系在一起了,那么我们需要更新对应的SnippetSerializer。在serializers.py的序列定义(serializer definition)中添加以下字段:

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

注意;去报你也添加'owner',到内部类Meta的字段列表里。 这个字段很有趣。source参数控制哪个属性被用于构成一个字段,并且能够指出序列实例的任何属性。它也能想上面一样使用点标记(.),这中情况下他会横贯给定的属性,就是我们使用Django模板语言一样。 我们添加的字段是隐式ReadOnly类,与其他类相反,如CharFieldBooleanField,隐式ReadOnlyField总是只读的,用于序列化表示,但在数据非序列化时不能用于更新实例。这里我们也可以用CharField(read_only=True)

为视图添加需要的权限

snippets数据已经和用户联系在一起,我们想确保只有授权的用户可以创建、更新和删除snippet数据。 REST框架包括许多权限类(permission classes),我们可以使用这些权限类来现在视图的访问权限。这种情况下,其中我们需要IsAuthenticatedOrReadOnly,这个类确保授权请求有读写权限,而没有授权的用户只有只读权限。 首先,在视图模块中引入以下代码

from rest_framework import permissions

然后,在SnippetListSnippetDetail视图类中添加以下属性。

permission_classes = (permissions.IsAuthenticatedOrReadOnly, )

在浏览器API中添加登录

如果你现在用浏览器打开API,你会发现你已经不能创建新的snippets数据。为此,我们需要以用户身份登录。 为了使用浏览器打开API,我们需要添加一个登录视图,编辑URL配置(URLconf)文件urls.py文件。 在urls.py顶部添加下面import。

from django.conf.urls import include

并且,在urls.py底部为API添加一个包括登录和退出视图的url样式。

urlpatterns += [
    url(r'^api-auth/', include('rest_framework.urls',
                               namespace='rest_framework')),
]

url样式的r'^api-auth/'部分实际上可以是任何你想要的URL。唯一的限制就是include的链接必须使用'rest_framework'名字空间。在Django 1.9+,REST框架会设置名字空间,所以你必须写。 现在如果你刷新浏览器页面,你会看到右上角的'Login'链接。如果你用之前创建的用户登录,你就可以再次写snippets数据了。 一旦你创建snippets数据,浏览'/users/',然后你会发现在每个用户的'snippets'字段,显示的内容包括与每个用户相关的snippets主键。

对象等级权限

虽然我们真的想任何人都和一看见snippets数据,但也要确保只有创建snippet的用户可以修改或删除他的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属性,添加自定义权限。

permission_classes = (permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly,)

确保引入了IsOwnerOrReadOnly类。

from snippets.permissions import IsOwnerOrReadOnly

现在,如果你再次打开浏览器,你会发现只有你登入,你才能删除(DELETE)或更新(PUT)属于你的snippet数据。

授权API

因为我们的API有一系列权限,所以如果我们想编辑任何snippets,我们需要授权我们的请求。我们现在还没有任何授权类(authenticaions classes),所以默认情况下只有SessionAuthenticationBasicAuthentication。 当我们通过Web浏览器与API交互时,我们可以登录,然后浏览器会话(session)将会提供必须的请求授权。 如果我们通过程序与API交互,我们需要为每个请求提供明确的授权证明。 如果我们在没有授权的情况下创建一个snippet,那么我们会得到下面的错误:

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"
}

总结

现在我们已经在我们的Web API上,为我们的系统用户和snippet的创建者,添加了很多权限和端点。 在第五部分,我们将会看怎么我们可以通过为我们的高亮snippets创建HTML端点来将所有东西联系在一起,然后在系统内用超链接将我们的API联系起来。