1. 程式人生 > 實用技巧 >Django Rest framework 之 認證

Django Rest framework 之 認證

在學習django rest framework(下面簡稱drf)之前需要知道

  • 對RESTful API設計有一定了解
    restful api設計風格
  • 對django框架有一定認識,本身drf就是基於django做的
  • 對python面向物件程式設計有了解(drf會對一些原生的django類做封裝)

一、前言

在學習drf之前的時候,先簡單說一下需要的預備知識。在django中,路由匹配之後,會進行路由分發,這個時候會有兩種選擇模式的選擇。也就是FBVCBV

1、FBV

fbv就是在url中一個路徑對應一個函式

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index)
]

在檢視函式中

def index(request):
    return render(request, 'index.html')

2、CBV

cbv就是在url中一個路徑對應一個類,drf主要使用CBV

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.IndexView.as_view())     # 執行類後面的as_view()方法,是父類裡面的方法
]

在檢視函式中

from django.views import View


class IndexView(View):
  
    # 以get形式訪問會執行get函式,一般情況下獲取資料
    def get(self, *args, **kwargs):  
        return HttpResponse('666')
      
    # 以post形式訪問的話會執行post函式,一般情況下發送資料
    def post(self, *args, **kwargs):  
        return HttpResponse('999')

我們在路由匹配的時候看到url(r'^index/', views.IndexView.as_view()),那這個as_view()是什麼,既然我們在檢視類中沒有定義這個as_view()方法,就應該到父類(也就是IndexView的父類View)中看一下View。以下是django原始碼,路徑是\django\views\generic\base.py

class View:

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']  # 支援的各種http方法

    def __init__(self, **kwargs):
         pass

    @classonlymethod
    def as_view(cls, **initkwargs):  # url路由匹配進入as_view方法
        def view(request, *args, **kwargs):
            return self.dispatch(request, *args, **kwargs)  # 返回dispath方法
        return view

    def dispatch(self, request, *args, **kwargs):  # dispath方法是drf的關鍵,dispath方法會通過反射,通過請求的方法,分發到各個檢視類的方法中
        pass

3、django的請求週期

因此根據CBV和FBVdjango的生命週期可以又兩類

  • FBV:請求通過uwsgi閘道器,中介軟體,然後進入路由匹配,進入檢視函式,連線資料庫ORM操作,模板渲染,返回經過中介軟體,最終交給瀏覽器response字串。
  • CBV:請求通過uwsgi閘道器,中介軟體,然後進入路由匹配,這裡就與FBV有區別了,因為不再是試圖函式而是檢視類,說的詳細一點,先經過父類View的dispath方法,進行請求方法的判斷,在分發到檢視類的方法,連線資料庫ORM操作,模板渲染,返回經過中介軟體,最終交給瀏覽器response字串。

而再drf中主要使用CBV,生命週期就變成了如下
請求通過uwsgi閘道器,中介軟體,然後進入路由匹配,這裡就有區別了,先經過drfAPIView類中的dispath方法(這裡假定檢視類沒有重寫APIView中的dispath方法),在dispath中對request請求進行封裝,反射回到檢視類,連線資料庫ORM操作,模板渲染,返回經過中介軟體,最終交給瀏覽器響應字串。

4、面向物件

說到面向物件就是三個特性,封裝,多型,繼承。

<1>、子類重寫父類方法

我們在繼承父類的時候往往會重寫父類中的方法,例如

class A:
    def get_name(self):
        return self.name
    def return_name(self):
        if hasattr(self, 'name'):
            return 'name: ' + getattr(self, 'name', None)    
    
    
class B(A):
    name = "b"
    def get_name(self):
        return self.name
    
b = B()
b.get_name()  # 輸出B
b.return_name()  # 輸出name: B,這裡由於B類中沒有實現return_name方法,例項化B得到b之後,會呼叫父類A中的return_name方法,hasattr方法會查詢類中是否有name屬性,這裡雖然在類A中沒有,會向下查詢B類中是否有name屬性,然後返回'name: ' + getattr(self, 'name', None)  ,也就是name:b

這是簡單的子類方法重寫父類中的方法,我們再使用drf的認證,許可權等元件是會經常對父類中的方法重寫,從而細粒度的實現自己的功能。
請注意:事情往往不是絕對的,如果像重寫python內建的基本資料型別,如字典,列表中的特殊方法,就會的到意想不到的結果,就是例項化的物件不再呼叫你重寫的方法,而是呼叫本來的方法。這是因為python的一些基本型別的方法是由c語言編寫的,python為了滿足速度,抄近道不會再呼叫你重寫的特殊方法。

<2>、mixin模式

class X(object):
    def f(self):
        print( 'x')

class A(X):
    def f(self):
        print('a')

    def extral(self):
        print('extral a')

class B(X):
    def f(self):
        print('b')

    def extral(self):
        print( 'extral b')

class C(A, B, X):
    def f(self):
        super(C, self).f()
        print('c')

print(C.mro())

c = C()
c.f()
c.extral()

這樣做也可以輸出結果

[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.X'>, <class 'object'>]  # 繼承的順序是 A-->B-->X-->object 這了的object在python3中是一切類的基類,包括object類本身。
a
c
extral a  # 雖然類C中沒有實現介面extral(),卻呼叫了父類A中的extral()方法

這樣的繼承雖然可以實現功能,但是有一個很明顯的問題,那就是在面向物件中,一定要指明一個類到底是什麼。也就是說,如果我想構造一個類,假如是Somthing,那麼我想讓這個類實現會飛,會游泳,會跑,三種行為,我可以這樣做,同時繼承,鳥,魚,馬三個類,像這樣

class Bird:
    def fly(self):
        print('fly')

class Fish:
    def swim(self):
        print('swim')
        
class Horse:
    def run(self):
        print('run')
        
        
class Something(Bird, Fish, Horse):
    pass


s = Something()
s.fly()
s.swim()
s.run()

輸出

fly
swim
run

可是實現會跑,會飛,會游泳的三種行為,但是這個類到底是什麼,是魚,是馬,還是鳥,也就是說不知道Something到底是個什麼類。為了解決這個問題,我們可以引用mixin模式。改寫如下

class BirdMixin:
    def fly(self):
        print('fly')

class FishMixin:
    def swim(self):
        print('swim')
        
class Horse:
    def run(self):
        print('run')
        
        
class Something(BirdMixin,  FishMixin,  Horse):
    pass

這樣就解決了上面的問題,也許你會發現,這其實沒有什麼變化,只是在類的命名加上了以Mixin結尾,其實這是一種預設的宣告,告訴你,Something類其實是一種馬,父類是HorseHorse,繼承其他兩個類,只是為了呼叫他們的方法而已,這種叫做mixin模式,在drf的原始碼種會用到。
例如drf中的generics 路徑為rest_framework/generics.py

class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    pass


class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    pass


class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    pass

相當於每多一次繼承,子類可呼叫的方法就更多了。

二、生成專案

1、生成專案

這裡可以使用pycharm作為整合開發工具,建立django專案檢視Python和第三方庫原始碼很方便,使用pycharm建立一個django專案,然後將django rest framework作為第三方包放入django專案中

2、資料庫設計

先來看一下如果不使用drf怎麼進行使用者認證,通常是用欄位驗證的方式,來生成相應的資料庫,在使用者登入時候,對資料庫查詢,簡單的資料庫設計如下

from django.db import models


class UserInfo(models.Model):
    USER_TYPE = (
        (1,'普通使用者'),
        (2,'VIP'),
        (3,'SVIP')
    )

    user_type = models.IntegerField(choices=USER_TYPE, default=1)
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)

class UserToken(models.Model):
    user = models.OneToOneField(UserInfo,on_delete=models.CASCADE)
    token = models.CharField(max_length=64)

簡單的使用者資訊,每個使用者關聯一個一對一的usertoken做為驗證
然後在專案目錄下執行生成資料庫命令

    python manage.py makemigrations
    python manage.py migrate

3、路由系統

from django.contrib import admin
from django.urls import path
from django.conf.urls import url

from api.views import AuthView


urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^api/v1/auth/$', AuthView.as_view())
]

api/v1/auth/中的api分別代表介面和版本號,後面會說到

4、檢視函式

  • md5函式根據使用者名稱和使用者的訪問時間進行加密
  • 當用戶第一次訪問時,資料庫建立使用者,並將token字串,儲存到資料庫
  • 當用戶下次訪問的時候,需要帶著這個字串與資料庫比對,並返回相應的提示資訊
    這裡的token暫時沒有放回瀏覽器端,真正專案中可以寫入到瀏覽器cookie
from django.shortcuts import render, HttpResponse
from django.http import JsonResponse
from django.views import View

from api import models


def md5(user):
    import hashlib
    import time

    # 當前時間,相當於生成一個隨機的字串
    ctime = str(time.time())

    # token加密
    m = hashlib.md5(bytes(user, encoding='utf-8'))
    m.update(bytes(ctime, encoding='utf-8'))
    return m.hexdigest()


class AuthView(View):

    def get(self, request, *args, **kwargs):
        ret = {'code': 1000, 'msg': 'success', 'name': '偷偷'}
        ret = json.dumps(ret, ensure_ascii=False)

        return HttpResponse(ret)

    def post(self, request, *args, **kwargs):
        ret = {'code': 1000, 'msg': None}
        try:
            user = request.POST.get('username')
            pwd = request.POST.get('password')
            obj = models.UserInfo.objects.filter(username=user).first()

            if not obj:
                # 如果使用者第一次登陸則建立使用者
                obj = models.UserInfo.objects.create(username=user, password=pwd)
                ret['code'] = 1001
                ret['msg'] = '建立使用者成功'

            # 為使用者建立token
            token = md5(user)
            # 存在就更新,不存在就建立
            models.UserToken.objects.update_or_create(user=obj, defaults={'token': token})
            ret['token'] = token
        except Exception as e:
            ret['code'] = 1002
            ret['msg'] = '請求異常'
        return JsonResponse(ret)

第一次傳送請求

返回請求資訊

第二次傳送請求

返回請求資訊

這裡沒有使用drf的認證元件

三、使用Django rest framewok 認證元件

1、例項

假如使用者想獲取自己的訂單資訊,傳送請求之後返回訂單資訊以json格式的資料返回。

from rest_framework.views import APIView
from django.http import JsonResponse
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions

from api import models


# 這裡直接表示訂單
ORDER_DICT = {
    1:{
        'name':'apple',
        'price':15
    },
    2:{
        'name':'狗子',
        'price':100
    }
}


class FirstAuthenticate(BaseAuthentication):
    # 新增自己的認證邏輯,基類BaseAuthentication中有一個必須要重寫的介面

    def authenticate(self, request):
        pass

    def authenticate_header(self, request):
        pass


class MyAuthenticate(BaseAuthentication):
    # 新增自己的認證邏輯,基類BaseAuthentication中有兩個必須要重寫的介面

    def authenticate(self, request):
        token = request._request.GET.get('token')  # 獲取token引數
        token_obj = models.UserToken.objects.filter(token=token).first()  # 在資料庫UserToken查詢是否有相應的物件
        if not token_obj:  # 如果沒有,則報錯
            raise exceptions.AuthenticationFailed('使用者認證失敗')
        return (token_obj.user, token_obj)  # 這裡需要返回兩個物件,分別是UserInfo物件和UserToken物件
  
    def authenticate_header(self, request):  # 返回相應頭資訊
          pass


class OrderView(APIView):
    # 使用者想要獲取訂單,就要先通過身份認證、
    # 這裡的authentication_classes 就是使用者的認證類
    authentication_classes = [FirestAuthenticate, MyAuthenticate]
    
    def get(self, request, *args, **kwargs):
        ret = {
            'code': 1024,
            'msg': '訂單獲取成功',
        }
        try:
            ret['data'] = ORDER_DICT
        except Exception as e:
            pass
        return JsonResponse(ret)

這裡繼承了rest framek中的APIView,在APIView中將原生的request進行了封裝,封裝了一些用於認證,許可權的類,在請求來的時候,會依次通過FirestAuthenticate MyAuthenticate兩個類,並呼叫authenticate進行認證。
傳送請求

返回訂單的資料

認證成功

2、原始碼分析

這裡推薦使用pycharm作為整合開發工具,可以ctrl+滑鼠左鍵點選方法,或者類直接進入原始碼檢視

<1>、第1步

在路由匹配之後會先進入到APIView中的as_view方法中,然後進入到djangoView中,

<2>、第2步

由於子類APIView已經實現了dispath方法,接著返回APIView中的disapth方法

<3>、第3步

然後會發現drf對原生request做的操作

<4>、第4步

這裡的initialize_request,主要進行封裝

<5>、第5步

而initial則會對呼叫封裝類中的方法,實現各種功能

至此可以看到requestdrf中大概的流程。

3、drf認證流程

在上面第4步和第5步可以看到APIView中的兩個方法的initialize_request,initial

我們進入到initialize_request,檢視authenticators=self.get_authenticators()

這裡的authentication_classes,其實是一個所有認證類的集合(指的是一個可以迭代的容器物件,如list,tuple等,而不是特指set()內建型別),

這裡的api_settings其實就是django專案的全域性配置檔案settings.py,這說明我們可以在需要認證的檢視函式多的情況下使用全域性配置使得每一個進行認證。

<1>、全域性與區域性配置認證類

可以直接在settings.py中新增全域性配置項

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES':  ['api.utils.authenticate.FirstAuthenticate', 'api.utils.authenticate.MyAuthenticate'],
}

那麼如果我的個別檢視類不想認證呢?可以這樣寫

class OrderView(APIView):
    # 這裡沒有重寫authentication_classes屬性,則使用全域性配置的authentication_classes,即在setting.py中的authentication_classes。
    def get(self, request, *args, **kwargs):
          pass

class CartView(APIView):
     authentication_classes = [authenticate.FirstAuthenticate,]  # authentication_classes中只包含FirstAuthenticate,則只通過他的認證
    def get(self, request, *args, **kwargs):
          pass

class UserInfoView(APIView):
    authentication_classes = []  #  authentication_classes為空,則不會進行認證
    def get(self, request, *args, **kwargs):
          pass

<2>、究竟如何進行認證

上面說了想要定義多個認證規則,其實就是封裝多個認證類,那麼這些認證類如何進行認證呢?

這裡的perform_authentication就是進行主要的功能,在request類中有一個_authenticate

來分析下原始碼

    def _authenticate(self):
        """
        Attempt to authenticate the request using each authentication instance
        in turn.
        """
        for authenticator in self.authenticators:  # 找到 authentication_classes,並迴圈每一個認證類
            try:
                user_auth_tuple = authenticator.authenticate(self)  # 呼叫認證類的authenticate方法,也就是上面我們實現的方法,並將返回值賦值給user_auth_tuple
            except exceptions.APIException:
                self._not_authenticated()  # 如果出錯呼叫_not_authenticated,方法,下面會說到
                raise

            if user_auth_tuple is not None:  # 如果authenticate方法的返回值不為空
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple  # 這也就是為什麼認證類的authenticate方法會返回兩個物件的原因
                return

        self._not_authenticated()  # 如果沒有通過認證,則呼叫_not_authenticated方法

    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()
        else:
            self.user = None

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

_authenticate方法中呼叫authenticator.authenticate(self) 方法,返回給user_auth_tuple,並通過判斷user_auth_tuple是否為空,其實就像是我從瀏覽器傳送請求,request中攜帶我的使用者認證資訊,在進入檢視類之前,通過一次一次呼叫認證類來檢視我攜帶的認證資訊是否正確,如果正確則返回資料庫中正確的User物件。如果不通過或者沒有認證資訊,則在_not_authenticated中按照匿名使用者處理。
來看一下authenticator.authenticate(self)中的authenticate(self)具體做了什麼

在authenticate中可以新增具體的認證邏輯,當然也可以在檢視類中書寫,但是drf中提供的元件,可以使得程式碼耦合度更低,維護性更強,更方便。

<3>、匿名使用者認證

上面_not_authenticatedUNAUTHENTICATED_TOKENUNAUTHENTICATED_USER說明,也可以通過在setting.py中定義匿名使用者的認證。
只要再setting.py中新增如下

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': ['api.utils.authenticate.FirstAuthenticate', 'api.utils.authenticate.MyAuthenticate'],
    "UNAUTHENTICATED_USER": None, # 匿名,request.user = None
    "UNAUTHENTICATED_TOKEN": None,# 匿名,request.auth = None
}

4、認證總結

要理解django rest framework ,就要先理解面向物件。子類繼承父類屬性和方法,而在基類中往往以定義抽象介面的形式,強制使子類重寫抽象介面。不過抽象介面這往往是框架開發者做的,而不是我們要需要做的。例項化的物件可以呼叫所類的屬性和方法,其實方法也可以看作是一種屬性。子類新定義或者重寫父類的屬性,例項化的物件可以呼叫父類中的方法查詢到子類的屬性,就是說例項化的物件集所有父類子類於一身。子類中的方法或者屬性會覆蓋掉父類中的方法和屬性,例項化物件呼叫的時候不會管父類中怎麼樣,所以在變數和方法命名的時候應該注意,或者也可以使用super等操作。
而在django rest framework中,對原生request做了封裝。原本我們可以再檢視類中的進行的比如訪問限流,使用者認證,許可權管理等邏輯,封裝到一個一個類中的方法中,在使用者請求進入檢視類之前,會先查詢並迭代相關封裝的類,然後呼叫這些類的相關方法,根據返回值判斷是否滿足認證,許可權等功能。如果不通過則不會進入到檢視類執行下一步,並返回相應的提示資訊。這樣分開的好處是當然就是最大程度的解耦,各個相關功能相互不影響,又相互關聯,維護性更高。