1. 程式人生 > >restframework(5):認證元件

restframework(5):認證元件

認證功能簡介

Django 自帶一個使用者認證系統,這個系統處理使用者帳戶、組、許可權和基於 cookie 的會話。本文將一步步的解讀restframework中的認證類。

認證功能說明

  1. 使用者登陸後才能訪問某些頁面。

  2. 如果使用者沒有登入就訪問該頁面的話直接跳到登入頁面。

  3. 使用者在跳轉的登陸介面中完成登陸後,自動訪問跳轉到之前訪問的地址。

restframework中的認證

在restframework中,認證即是通過繼承BaseAuthentication重構認證的類,認證的邏輯在類的authenticate方法中實現,通過判斷使用者傳遞的資訊,如果認證成功返回(使用者,使用者Token)元組,會將使用者物件封裝到request裡,通過request.使用者可以獲得使用者的相關資訊。

認證快速例項

快速案例:自定義認證類

我們針對第三篇博文的出版社、圖書、作者另外加上下面兩個類: models.py

class User(models.Model):
    name=models.CharField(max_length=32)
    pwd=models.CharField(max_length=32)

class Token(models.Model):
    user=models.OneToOneField("User")
    token = models.CharField(max_length=128)

    def __str__(self)
: return self.token

首先我們在models中建立兩張表,其中一張是我們的登入表,為user,另一張為校驗表,裡面的字元為token值,表的關係為一對一的形式,這裡為什麼要把token單獨拿出來,而不是放在一張表裡,因為token是一個校驗欄位,一般如果手段建立表,最好還是將token放在額外的一張表裡好,這麼做的好處第一是可以提高伺服器效率,第二是也為爬蟲能提高效率有關。

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication

from
.models import Token class TokenAuth(BaseAuthentication): def authenticate(self,request): token = request.GET.get("token") token_obj = Token.objects.filter(token=token).first() if not token_obj: raise exceptions.AuthenticationFailed("驗證失敗123!") else: return token_obj.user.name,token_obj.token

上面是自定義的一個使用者認證類,基本完成了認證的功能,這裡需要注意的是在TokenAuth類中,類名是可以改的,只要是遵循了大駝峰命名法,其它隨意,但方法名不能變,就和前面我介紹過的序列化元件一樣,它內部原始碼已經固定了,所以如果不看裡面的原始碼最好還是不要去加以修改,否則根據對映關係,找不到方法名那麼就不再具有認證效果。

區域性使用

class AuthorModelView(viewsets.ModelViewSet):
    authentication_classes = [TokenAuth,]
    ...

另外這個繼承的是我們自定義認證類,那麼當程式執行時,它會自動去判斷token對不對,也就是有沒有登入,如果沒有,那麼會報401和403的錯誤。

  • 401 Unauthorized 未認證
  • 403 Permission Denied 許可權被禁止

而如果我們不想自己寫認證類,那麼我們可以看到在我們寫的認證類中,其實是繼承了BaseAuthentication,也就是說restframework中幫我們封裝好了認證許可權元件,那麼我們其實可以直接使用,在上一篇JWT中也介紹過認證總共有四個模組,分別為BaseAuthentication、SessionAuthentication、TokenAuthentication、JSONWebTokenAuthentication,具體的內容可以看上一篇博文。所以我們還可以這樣設定:

from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.views import APIView

class ExampleView(APIView):
    authentication_classes = (SessionAuthentication, BasicAuthentication)
    ...

全域性使用

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',   # 基本認證
        'rest_framework.authentication.SessionAuthentication',  # session認證
    )
}

這種就是全域性的預設配置了,它會自動去呼叫內建的表,去檢視有沒有使用者儲存的記錄,如果沒有就會拋401或者403,如果我們配置了全域性的,但有一些區域性的類並不想用它的功能,因為並不是所有操作都需要認證,比如說我登入一下首頁,那麼我們就可以在類裡面加一句:

    authentication_classes = []

表示不接受此認證功能。

使用認證類生成token

class LoginView(APIView):
    authentication_classes = []
    def post(self,request):

        name=request.data.get("name")
        pwd=request.data.get("pwd")
        user=User.objects.filter(name=name,pwd=pwd).first()
        res = {"state_code": 1000, "msg": None}
        if user:

            random_str=get_random_str(user.name)
            token=Token.objects.update_or_create(user=user,defaults={"token":random_str})
            res["token"]=random_str
        else:
            res["state_code"]=1001 #錯誤狀態碼
            res["msg"] = "使用者名稱或者密碼錯誤"

        import json
        return Response(json.dumps(res,ensure_ascii=False))

當我們利用postman登入驗證正確後:

在這裡插入圖片描述

否則會出現:

在這裡插入圖片描述

這裡通過判斷輸入值和資料庫值是否匹配,然後再用update_or_create方法經過md5生成,可能你還會有很多的疑問,因為目前的東西並不好理解,那麼下面就看看原始碼是怎麼運作的。

認證原始碼解析

在這裡插入圖片描述

這是我用億圖圖示畫的流程圖,思維導圖之前有嘗試,但效果不好就刪了。上圖中的菱形是我們使用者需要自己定義的內容,然後矩形是原始碼中的類名,圓形是方法名,大概整個認證的流程就是圍繞著這三個類去週轉的。(這裡為啥是粉紅色,因為選擇基礎的流程圖預設就是。。。一般用Visio,不過發現都還好用)

View -> dispatch方法

前面的檢視的博文中我們分析過了,整個檢視原始碼,最精華的部分可能就是dispatch了,因為它作為其它所有函式的啟動函式,認證也不例外:

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進行加工,豐富了一些功能
        #Request(
        #     request,
        #     parsers=self.get_parsers(),
        #     authenticators=self.get_authenticators(),
        #     negotiator=self.get_content_negotiator(),
        #     parser_context=parser_context
        # )
        #request(原始request,[BasicAuthentications物件,])
        #獲取原生request,request._request
        #獲取認證類的物件,request.authticators
        #1.封裝request
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            #2.認證
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

這個原始碼介紹得很詳細了,這裡我再提一點,在dispatch函式中的分發之前,還有上面這樣一句程式碼,而認證、頻率、驗證元件都在這裡面,那就是initial,我們此篇博文主要的邏輯也是在此。

initial初始化

    def initial(self, request, *args, **kwargs):
        """
        Runs anything that needs to occur prior to calling the method handler.
        """
        self.format_kwarg = self.get_format_suffix(**kwargs)

        # Perform content negotiation and store the accepted info on the request
        neg = self.perform_content_negotiation(request)
        request.accepted_renderer, request.accepted_media_type = neg

        # Determine the API version, if versioning is in use.
        version, scheme = self.determine_version(request, *args, **kwargs)
        request.version, request.versioning_scheme = version, scheme

        # Ensure that the incoming request is permitted
        self.perform_authentication(request)  # 認證功能
        self.check_permissions(request)  # 許可權功能
        self.check_throttles(request)  # 頻率功能

前面的程式碼都不是很重要,無非就是程式碼執行前的一些準備工作,我們需要看的是self.perform_authentication(request)這個語句呼叫了什麼函式:

    def perform_authentication(self, request):
        """
        Perform authentication on the incoming request.

        Note that if you override this and simply 'pass', then authentication
        will instead be performed lazily, the first time either
        `request.user` or `request.auth` is accessed.
        """
        request.user

僅僅呼叫了request.user?認證邏輯為:如果user取到,就執行許可權;取不到就pass掉。那麼假如我們想要做一些逾越認證許可權的功能,甚至也可以直接在類裡呼叫這個函式,然後直接pass。

那麼迴歸正題,這裡就需要我們去找到request被呼叫的地方。

request的封裝與初始化

    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
        )

這裡也沒啥好看的,返回了一大堆引數,但我們要找的user就在Request類裡面:

    @property
    def user(self):
        """
        Returns the user associated with the current request, as authenticated
        by the authentication classes provided to the request.
        """
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                #獲取認證物件,進行一步步的認證
                self._authenticate()
        return self._user

點進去後我們發現Request有個user方法,加 @property 表示呼叫user方法的時候不需要加括號“user()”,這就相當於將其轉化成了靜態方法,方便以後可以直接呼叫:request.user

_authenticate()和_not_authenticated函式

_authenticate()函式:

    def _authenticate(self):
        """
        Attempt to authenticate the request using each authentication instance
        in turn.
        """
        #迴圈認證類的所有物件
        #執行物件的authenticate方法
        for authenticator in self.authenticators:
            try:
                #執行認證類的authenticate方法
                #這裡分三種情況
                #1.如果authenticate方法丟擲異常,self._not_authenticated()執行
                #2.有返回值,必須是元組:(request.user,request.auth)
                #3.返回None,表示當前認證不處理,等下一個認證來處理
                user_auth_tuple = authenticator.authenticate(self)
            except exceptions.APIException:
                self._not_authenticated()
                raise

            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple
                return

        self._not_authenticated()

_not_authenticate()函式:

    def _not_authenticated(self):
        """
        Set authenticator, user & authtoken representing an unauthenticated request.

        Defaults are None, AnonymousUser & None.
        """
        self._authenticator = None

        if api_settings.UNAUTHENTICATED_USER:
            self.user = api_settings.UNAUTHENTICATED_USER()   #AnonymousUser匿名使用者
        else:
            self.user = None

        if api_settings.UNAUTHENTICATED_TOKEN:
            self.auth = api_settings.UNAUTHENTICATED_TOKEN()  #None
        else:
            self.auth = None

那麼到這裡,認證邏輯基本就都通了,當有這個使用者時,會將user返回回去,但如果沒有這個使用者,那麼將會是AnonymousUser 匿名使用者。

另外,我們還可以看看restframework向Request類中多加的認證引數,self.get_authenticators(),這個我在流程圖裡面也畫了,與之對應的是我們在類中寫的authentication_classes,只要我們使用者中寫了這個引數,那麼它就會覆蓋原始碼中的預設值,我們也可以來看一下原始碼流程:

    def get_authenticators(self):
        """
        Instantiates and returns the list of authenticators that this view can use.
        """
        return [auth() for auth in self.authentication_classes]

authentication_classes中:

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS

總結

一直想寫認證,現在終於大致過了一遍,不能說寫得多通透,至少還有很多小細節點可能目前還沒注意到,另外不知不覺中寫了這麼多,中途還發生過一些小插曲,就是電腦宕機,而csdn又不像word能自動儲存,本來是想許可權和認證一起寫的,那麼就肯定沒有單寫考慮得那麼多了,但就更能突出對比性。只能說各有優劣。然後我說的小細節就是,authenticate這個功能的後面,如果我們使用者不給它賦值,那麼它就會找到父類中的配置去,但這裡我沒有想下去了,找了一些別的博文看了下,發現最後會返回user和token的元組,還有我用億圖畫的導圖其實還能精簡些,感覺有些醜了。。。算了,目前就那樣吧。

參考文獻: