Django(62)自定義認證類

Silent丿丶黑羽發表於2021-06-13

前言

如果我們不用使用drf那套認證規則,我們想自定義認證類,那麼我們首先要知道,drf本身是如何定義認證規則的,也就是要檢視它的原始碼是如何寫的
 

原始碼分析

原始碼的入口在APIView.py檔案下的dispatch方法下的self.initial方法中的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

返回了一個requestuser方法,request代表的是drfRequest,所以我們進入drfRequest類中查詢user方法屬性,原始碼如下:

    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

上述程式碼的意思是:返回與當前請求關聯的使用者,由提供給請求的身份驗證類進行身份驗證。如果沒有使用者,我們需要通過_authenticate方法驗證,我們檢視下它的原始碼

def _authenticate(self):
    """
    嘗試依次使用每個身份驗證例項對請求進行身份驗證。
    """
    for authenticator in self.authenticators:
        try:
            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()

我們可以看到self.authenticators驗證器其實是呼叫父類APIViewauthenticatorsAPIViewauthenticators在原始碼initialize_request方法下的get_authenticators,我們檢視原始碼

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檢視

authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

我們就知道了drf預設的認證器在settings檔案下的DEFAULT_AUTHENTICATION_CLASSES類下面

'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication'
    ],

我們發現drf預設有2個認證類一個基礎的認證,另一個session認證,這兩個認證類都繼承自BaseAuthentication,我們來看下原始碼

class BaseAuthentication:
    """
    所有的認證類都繼承自BaseAuthentication.
    """

    def authenticate(self, request):
        """
         認證請求返回一個二元組(user, token),並且此方法必須重寫,否則丟擲異常
        """
        raise NotImplementedError(".authenticate() must be overridden.")

    def authenticate_header(self, request):
        """
        Return a string to be used as the value of the `WWW-Authenticate`
        header in a `401 Unauthenticated` response, or `None` if the
        authentication scheme should return `403 Permission Denied` responses.
        """
        pass

接下來我們看下BasicAuthentication如何寫的,後續我們依葫蘆畫瓢

class BasicAuthentication(BaseAuthentication):
    """
    針對使用者名稱密碼的 HTTP 基本身份驗證
    """
    www_authenticate_realm = 'api'

    def authenticate(self, request):
        """
        如果使用 HTTP 基本身份驗證提供了正確的使用者名稱和密碼,則返回“User”。否則返回“None”。
        """
        # 獲取請求頭中`HTTP_AUTHORIZATION`,並進行分割
        auth = get_authorization_header(request).split()
        
        # 如果沒有auth或者auth的第一個索引值的小寫不等於basic,則返回None
        if not auth or auth[0].lower() != b'basic':
            return None
        
        # auth列表的長度必須等於2,格式['basic', 'abc.xyz.123']
        # 如果auth的長度等於1,則丟擲異常
        if len(auth) == 1:
            msg = _('Invalid basic header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        # 如果長度大於2,也丟擲異常
        elif len(auth) > 2:
            msg = _('Invalid basic header. Credentials string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            try:
                # auth[1]解碼格式為utf-8
                auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
            except UnicodeDecodeError:
                auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
            auth_parts = auth_decoded.partition(':')
        except (TypeError, UnicodeDecodeError, binascii.Error):
            msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
            raise exceptions.AuthenticationFailed(msg)

        userid, password = auth_parts[0], auth_parts[2]
        return self.authenticate_credentials(userid, password, request)

    def authenticate_credentials(self, userid, password, request=None):
        """
        Authenticate the userid and password against username and password
        with optional request for context.
        """
        credentials = {
            get_user_model().USERNAME_FIELD: userid,
            'password': password
        }
        user = authenticate(request=request, **credentials)

        if user is None:
            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))

        if not user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (user, None)

    def authenticate_header(self, request):
        return 'Basic realm="%s"' % self.www_authenticate_realm

 

自定義認證類

  1. 建立繼承BaseAuthentication的認證類
  2. 實現authenticate方法
  3. 實現體根據認證規則 確定 遊客 正常使用者 非法使用者
  4. 進行全域性或區域性配置(一般採用全域性配置)

認證規則

  1. 沒有認證資訊,返回None(遊客)
  2. 有認證資訊認證失敗,拋異常(非法使用者)
  3. 有認證資訊認證成功,返回使用者和認證資訊的元組(合法使用者)

我們建立一個資料夾authentications,寫入如下程式碼

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from api.models import User


class MyAuthentications(BaseAuthentication):

    def authenticate(self, request):
        # 前臺在請求頭攜帶認證資訊
        # 且預設規範用Authorization欄位攜帶認證資訊
        # 後臺固定在請求物件的META欄位中的HTTP_AUTHORIZATION獲取
        auth = request.META.get('HTTP_AUTHORIZATION', None)

        # 處理遊客
        if auth is None:
            return None

        auth_list = auth.split()
        if not len(auth_list) == 2 and auth_list[0].lower() == "auth":
            raise AuthenticationFailed("認證資訊有誤,非法使用者")
        # 合法的使用者還需要從auth_list[1]中解析出來
        # 注:假設一種情況,資訊為xx.yy.zz,就可以解析出admin使用者:實際開發,該邏輯一定是校驗使用者的正常邏輯
        if auth_list[1] != 'xx.yy.zz':  # 校驗失敗
            raise AuthenticationFailed("使用者校驗失敗,非法使用者")

        user = User.objects.filter(username='jkc').first()
        print(user)

        if not user:
            raise AuthenticationFailed("使用者資料有誤,非法使用者")

        return user, None

然後在settings.py中配置全域性的自定義認證類

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'api.authentications.MyAuthentications'
    ],
}

最後寫入檢視函式

class TestView(APIView):
    def get(self, request, *args, **kwargs):
        return APIResponse(data_msg="drf get ok")

然後我們訪問檢視,在headers中不傳Authorization 代表遊客,遊客可以訪問成功

{
    "statusCode": 0,
    "message": "drf get ok"
}

接著我們在請求頭中只傳auth

訪問檢視會丟擲異常資訊

{
    "detail": "認證資訊有誤,非法使用者"
}

然後我們在請求頭中傳入錯誤的認證,auth 111

訪問檢視會丟擲異常資訊

{
    "detail": "使用者校驗失敗,非法使用者"
}

最後我們在請求頭中傳入正確的認證,auth xx.yy.zz,這次會得到正確的返回結果

{
    "statusCode": 0,
    "message": "drf get ok"
}

以上的測試,就代表我們自定義的認證類起作用了

相關文章