DRF比Django的認證和許可權高在哪裡

dongfanger發表於2020-12-20

Django可以用LoginRequiredMixinPermissionRequiredMixin給類檢視新增認證和許可權,DRF做了高階封裝,提供了更簡潔的實現方式。我們通過繼續學習官網教程來進行了解。

更新model

首先修改Snippet模型,新增2個欄位:owner,儲存snippet建立者,highlighted,儲存高亮HTML。同時重寫save方法,在同步資料庫的時候,使用pygments包把code格式化後存到highlighted欄位。修改後的snippets/models.py完整程式碼如下:

from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
    owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
    highlighted = models.TextField()

    class Meta:
        ordering = ['created']

    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(Snippet, self).save(*args, **kwargs)

接著刪除資料庫和migrations,重新遷移資料庫:

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

並建立超級管理員:

python manage.py createsuperuser

User新增Endpoint

Endpoint,表示API的具體網址。我們按照models.pyserializers.pyviews.pyurls.py的程式碼編寫順序,給User模型新增Endpoint。

models.py

直接使用Django預設User模型,不需要修改程式碼。

serializers.py

新增UserSerializer,由於User沒有snippets欄位,所以需要顯式新增:

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']

views.py

新增只讀的列表檢視UserList和詳情檢視UserDetail,分別用到了ListAPIViewRetrieveAPIView

from django.contrib.auth.models import User
from snippets.serializers import UserSerializer


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


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

urls.py

新增訪問路徑:

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

關聯User和Snippet

如果使用POST方法請求http://127.0.0.1:8000/snippets/,嘗試新增1條資料:

image-20201219145940635

會發現介面報錯了:

DRF比Django的認證和許可權高在哪裡

owner_id不能為空?因為前面只給Snippet新增了owner欄位,還沒有寫反序列化更新模型的程式碼,所以通過請求訪問檢視,再嘗試反序列化的時候,報錯了。我們先修改檢視SnippetList來修復這個問題:

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

SnippetList檢視中重寫perform_create()方法,意思是在儲存時,把request.user值賦給owner欄位。perform_create()方法的原始碼是:

class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

再修改snippets/serializers.py,新增owner欄位,支援序列化:

class SnippetSerializer(serializers.ModelSerializer):
    # ReadOnlyField表示只能序列化為JSON,不能反序列化更新模型
    # 也可以改成CharField(read_only=True)
    owner = serializers.ReadOnlyField(source='owner.username')

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

注意Meta.fields也要加上owner哦。

再請求一次:

image-20201219151728225

剛才的錯誤沒有了,但是報了個新的錯誤:Snippet.owner必須是User例項,給它賦值的是AnonymousUser(匿名使用者),導致ValueError了。這個報錯是發生這條程式碼:

serializer.save(owner=self.request.user)

也就是說請求訪問檢視後,進行反序列化了,但是反序列化失敗了。非常奇怪!我們的請求中並沒有使用者資訊,正常來說在訪問檢視的時候就該被攔截了。

給檢視新增認證

我們需要讓API更符合常規,讓未認證的使用者不能執行檢視中的程式碼。DRF提供了rest_framework .permissions來給檢視新增認證:

DRF比Django的認證和許可權高在哪裡

其中IsAuthenticatedOrReadOnly表示只有認證了才能讀寫,否則只能讀。把它新增到SnippetListSnippetDetail檢視中:

from rest_framework import permissions

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

再請求試試,剛才的錯誤沒有了,API返回的是需要提供使用者憑證:

image-20201219160601041

登入檢視

如果用瀏覽器開啟http://127.0.0.1:8000/snippets/,會發現只有GET方法沒有POST,這是因為需要新增DRF登入檢視,在tutorial/urls.py中新增rest_framework.urls

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

api-auth/可以自定義。

重新整理頁面右上角就會出現Log in按鈕,登入後就能POST了。

物件級許可權

為了更細粒度的控制許可權,讓使用者只能編輯自己建立的snippet,新建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

新增IsOwnerOrReadOnly許可權,繼承了permissions.BasePermission,重寫了has_object_permission()方法。接著在snippets/views.py中給SnippetDetail加上:

from snippets.permissions import IsOwnerOrReadOnly


permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]

試下訪問其他使用者建立的snippet,發現只能檢視:

image-20201219180310509

訪問自己建立的snippet,可以修改和刪除:

image-20201219180516850

自定義許可權

以上是官網的示例,我在Postman測試了下,發現超管dongfanger可以建立snippet

image-20201219152719121

普通使用者player也可以建立snippet

image-20201219153118906

我想讓普通使用者不能建立,只能超管建立。仿照官網示例,在snippets/permissions.py中新增IsAdminOrReadOnly

class IsAdminOrReadOnly(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.user.is_superuser

接著給SnippetList加上:

permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsAdminOrReadOnly]

用普通使用者嘗試建立,提示沒有許可權:

image-20201219181059751

用超級管理員嘗試建立,成功:

image-20201219181537178

其他認證方式

本文使用的認證方式是預設的SessionAuthenticationBasicAuthentication,只要資料庫的使用者名稱、密碼和請求中的使用者憑證(使用者名稱、密碼)匹配上了,就認為認證成功。如果要實現token或jwt認證,需要使用到rest_framework.authentication

DRF比Django的認證和許可權高在哪裡

rest_framework_jwt.authentication

DRF比Django的認證和許可權高在哪裡

pip install djangorestframework-jwt

這一部分內容官網教程中並沒有提及,等我們把教程學完了,以後再找時間來介紹。

東方說

DRF實現認證和許可權的關鍵在於新增permissions.py模組,編寫class,繼承permissions.BasePermission,重寫has_permission()has_object_permission()方法,再新增class到類檢視的permission_classes中。這塊的內容比Django的認證系統那套簡潔,但是有點混淆,另外我之前參照網上實現了一版JWT,也有點不一樣。看來還得寫篇對比的文章才行。

參考資料:

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/

相關文章