drf 認證校驗及原始碼分析

雲崖先生發表於2020-10-31

認證校驗

   認證校驗是十分重要的,如使用者如果不登陸就不能訪問某些介面。

   再比如使用者不登陸就不能夠對一個介面做哪些操作。

   drf中認證的寫法流程如下:

   1.寫一個類,繼承BaseAuthentication,並且覆寫其authenticate方法

   2.當認證通過後應該返回兩個值,並且第一個值會傳遞給request.user這個屬性中,第二個值將會傳遞給request.auth這個屬性中

   3.如果認證失敗,則丟擲異常APIException或者AuthenticationFailed,它會自動捕獲並返回

   4.當前認證類設定是全域性使用還是區域性使用

準備工作

   我們有一個登入功能,並且還有一個查詢商品的介面,只有當使用者登入後才能進行查詢,否則就不可以。

模型表

   兩張表如下:

from django.db import models


class User(models.Model):
    # 使用者
    user_id = models.AutoField(primary_key=True)
    user_name = models.CharField(max_length=32)
    user_password = models.CharField(max_length=32)
    user_token = models.CharField(max_length=64,unique=True,null=True)  # token,唯一

    def __str__(self):
        return self.user_name

    class Meta:
        db_table = ""
        managed = True
        verbose_name = "User"
        verbose_name_plural = "Users"

class Merchandise(models.Model):
    # 商品
    merchandise_id = models.AutoField(primary_key=True)
    merchandise_name = models.CharField(max_length=32)
    merchandise_price = models.IntegerField()

    def __str__(self):
        return self.merchandise_name

    class Meta:
        db_table = ""
        managed = True
        verbose_name = "Merchandise"
        verbose_name_plural = "Merchandises"

   使用者表的資料如下:

   image-20201031170412050

   商品表的資料如下:

   image-20201031171236819

   現在,只有當使用者登入後,才能夠訪問商品的介面。

   也就是說,使用者的token自動如果為空,將會被認為沒有登陸。

序列類

   下面是序列類,我們只展示商品,使用者列表將不會展示:

from rest_framework.serializers import ModelSerializer
from . import models

class MerchandiseModelSerializer(ModelSerializer):
    class Meta:
        model = models.Merchandise
        fields = "__all__"
        

檢視

   檢視,我們只寫了關於使用者登入與商品的介面:

from uuid import uuid4
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from . import models
from . import ser
from . import authLogin  # 匯入認證的檔案

class MerchandiseAPI(ModelViewSet):
    queryset = models.Merchandise.objects.all()
    serializer_class = ser.MerchandiseModelSerializer

class Login(APIView):
    def post(self,request):
        # 代表使用者登入
        login_msg = {
            "user_name": request.data.get("user_name"),
            "user_password": request.data.get("user_password"),
        }
        user_obj = models.User.objects.filter(**login_msg).first()
        if user_obj:
            token = uuid4()  # 生成隨機字串
            user_obj.user_token = token
            user_obj.save()
            return Response(data="登入成功",headers={"token":token})  # 返回隨機字串
        else:
            return Response(data="登入失敗,使用者名稱或密碼錯誤")

url

   使用自動生成路由:

from django.contrib import admin
from django.urls import path, re_path
from rest_framework.routers import SimpleRouter

from app01 import views

router = SimpleRouter()
router.register("merchandises",views.MerchandiseAPI)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/',views.Login.as_view()),
]
urlpatterns.extend(router.urls)


基本使用

認證類

   接下來我們要書寫一個認證類:

from rest_framework.authentication import BaseAuthentication  # 繼承的基類
from rest_framework.exceptions import AuthenticationFailed  # 異常
from . import models
from django.http import request

class LoginVerify(BaseAuthentication):
    def authenticate(self, request):
        token = request.META.get("HTTP_TOKEN")
        # 如果在請求頭中設定的是token的key名,獲取時一定要全大寫並加上HTTP
        if not token:
            raise AuthenticationFailed("請求失敗,請求頭中缺少token")
        else:
            user_obj = models.User.objects.filter(user_token=token).first()  # 獲取使用者物件
            if user_obj:
                return user_obj,user_obj.user_token  # 返回使用者本身和token。這樣request.user裡面就能拿到該使用者了
            else:
                raise AuthenticationFailed("token不存在,使用者不存在,請不要偽造登入")

區域性使用

   只需要在商品介面中設定一個類屬性,該介面便會進行認證。

class MerchandiseAPI(ModelViewSet):
    authentication_classes = [authLogin.LoginVerify]  # 使用認證
    queryset = models.Merchandise.objects.all()
    serializer_class = ser.MerchandiseModelSerializer 

全域性使用

   只需要在settings.py中進行配置。

REST_FRAMEWORK={
    "DEFAULT_AUTHENTICATION_CLASSES":["app01.authLogin.LoginVerify",]
}

   如果想取消某個介面的認證,則在其中設定類屬性authentication_classes是一個空列表。

   如下所示,登入功能不需要驗證,我們對他取消掉即可。

class Login(APIView):
	authentication_classes = []

原始碼分析

流程分析

   由於modelViewSet繼承自APIView,所以我們直接看as_view(),在下面這一句程式碼中,將會對request進行二次封裝。

    def dispatch(self, request, *args, **kwargs):

        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)  # 這裡
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

   在二次封裝中,例項化出了一個Request物件並返回了,在例項化時,會呼叫self.get_authenticators()方法,此時的self是我們自定義的檢視類,切記這一點。

    def initialize_request(self, request, *args, **kwargs):
        """
        Returns the initial request object.
        """
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),  # 看這裡,獲取認方式
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )

   下面是get_authenticators()的程式碼,可以看見它會迴圈self.authentication_classes這個可迭代物件,如果你沒有傳遞這個可迭代物件,那麼該物件是一個預設的設定。

    def get_authenticators(self):
        return [auth() for auth in self.authentication_classes] # ( authLogin.LoginVerify呼叫,例項化 )

   如果沒有傳遞,將會找到APIView中的預設設定:

	class APIView(View):
        renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
        parser_classes = api_settings.DEFAULT_PARSER_CLASSES
        authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES  # 預設的設定,預設的認證類,可以自己看一下

   如果有進行傳遞,可以發現它是使用了一個括號,這就代表會呼叫,由於傳入的是一個類,所以它會進行例項化。

   所以我們可以認為request.authenticators這個引數是一個tuple,裡面包含了認證類的例項化物件。

   然後,request就被二次包裝完畢了。接下來執行 self.initial(),現在的self依然是我們自定義的檢視類。

    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

   下面是self.inital()的程式碼,

    def initial(self, request, *args, **kwargs):

        self.format_kwarg = self.get_format_suffix(**kwargs)

        self.perform_authentication(request)  # 只看這個,認證相關的
        self.check_permissions(request)
        self.check_throttles(request)

   到了self.perform_authentication()時,它傳遞進了個request,並且會去找user這個屬性抑或是被property裝飾的方法,所以我們需要到Request這個類中去找,需要注意的是如果user是一個方法,這代表會自動傳遞進self,此時的self則是我們經過二次封裝的request物件。

   可以發現它是一個被裝飾的方法。很顯然我們沒有_user這個方法或屬性,會執行with語句,其實直接看self._authenticate()即可。再次強調,此次的self是二次封裝的request物件。

    @property
    def user(self):
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user

   下面是整個程式碼的核心。

    def _authenticate(self):
 
        for authenticator in self.authenticators:  # 迴圈認證類物件 ( authLogin.LoginVerify的例項化 )
            try:
                user_auth_tuple = authenticator.authenticate(self) # 這裡會找authenticate方法並將request物件進行傳遞,我們的認證類繼承了BaseAuthentication這個類,它會實現一個介面方法, 但會丟擲異常。
            except exceptions.APIException:   # 如果沒有實現介面方法,或在驗證時丟擲異常都會被這裡捕獲
                self._not_authenticated()  # 執行這裡 self.user將會是匿名使用者AnonymousUser,而self.auth則是None
                raise

            if user_auth_tuple is not None:  # 如果返回的值不是空
                self._authenticator = authenticator  
                self.user, self.auth = user_auth_tuple  # 分別賦值給self.user,以及self.auth中
                return  # 返回

        self._not_authenticated()  # 上面有認證物件就會return,沒有還是設定匿名使用者和None

最後總結

   其實看了原始碼後,你可以發現我們的認證類可以不繼承BaseAuthentication,但是推薦繼承會更規範,因為這個基類實現了抽象介面。

   其次,它將返回的兩個值分別賦值給了request.user以及request.auth

   如果你沒有返回值,那麼對應的,request.user就是匿名使用者,request.auth就是None

   如果你沒有配置認證類,其實它會走預設的認證類。

   老規矩,關於配置認證類時依舊是先用區域性的,再用全域性的,最後是用預設的,如果你的上面的原始碼確實有感覺了的話,應該能夠看懂。

相關文章