Django框架rest_framework中APIView的as_view()原始碼解析、認證、許可權、頻率控制

希希大隊長發表於2019-07-04

在上篇我們對Django原生View原始碼進行了區域性解析:https://www.cnblogs.com/dongxixi/p/11130976.html

在前後端分離專案中前面我們也提到了各種認證需要自己來做,那麼我們用rest_framework的時候

rest_framework也為我們提供相應的介面,rest_framework中的APIView實現了和Django原生View  as_view()一樣的功能

並在此基礎上實現了原生request的封裝、認證、許可權、檢視等等功能

 

 

我們來看APIView的as_view如何實現的:

 

 

 通過上篇對View原始碼的分析我們可以得知,在View的的閉包函式view中呼叫了dispatch方法,那麼我們在找dispatch的時候還是要從self開始找,

此時的self是我們在檢視層定義的檢視類的物件,檢視類並沒有定義dispatch,那麼就找父類APIView,在APIView中我們找到了dispatch

 

那麼意味著APIView對dispatch進行了重寫,我們來看看APIView怎麼封裝的dispatch方法:

 

 我們繼續深入initialize_request()去看看它是怎麼封裝原生request的:

 

 到這裡,我們可以知道,它將原生的request和認證等元件給到Request類例項化返回,我們仍然需要進一步去看Request的原始碼:

 

 這裡我們可以得知,它將原生的request封裝到了新的request物件的_request屬性中,那麼你就會想了,那原生request 的資料和方法都到哪裡去了呢?

彆著急,它也給你做了封裝,繼續看:

GET請求的資料:

 

 它將原生GET請求的資料放到了query_parms裡面,我們在檢視類中通過request.query_parms就可以取到

 

POST請求資料:

 

 

這裡的data不僅僅是POST的資料,所有請求方式的鍵值對資料的都會被放入data裡面,支援urlencoded、form-data、json(application/json)

 

 

 

FILES資料:

 

對USER的封裝:

 

 此外還封裝了auth等其他功能,這些功能不僅在新的request裡面有,它也同樣存在原生的request裡,在該方法的解釋上,它講明瞭它支援Django底層contrib,並將user設定在了Django原生的HttpRequest例項中,以保證在任何Django原生中介軟體都可以通過校驗,所以我們在使用APIView時可以幾乎不用考慮相容性問題

 

 好了,我們剛剛看完了initialize_request()原始碼,瞭解了APIView對原生request進行如何進行封裝之後

接下來我們來看initial是如何幫我們做認證、許可權等校驗的

 

 

認證校驗

 首先我們來看認證校驗

按住Ctrl點進去之後發現它就一行程式碼

request.user

那此時我們繼續找Request類中的user,

 

 發現它執行了_authenticate(),然後由於好奇心,我們繼續往下點:

 

 通過上圖的分析我們得知,self.authenticators 這個裡面裝著一個個的認證類物件,那麼這些認證類物件是哪裡來的呢?我們繼續探究:

 我們回到Request封裝原生request例項化的地方看傳入的是什麼東西

 

我們繼續找 get_authenticators()方法!

 

 發現是從self.authentication_classes中拿到的,果然返回的是一個裝有物件的列表

我們繼續找authentication_classes

我們在檢視類中沒有定義authentication_classes屬性,那麼繼續往上找發現:

 

 這時候看到它是從api_settings裡找的,我們或多或少應該明白了,它會從使用者配置中去找這個屬性,但是我們並沒有早配置檔案中配置這個屬性,也就意味self.get_authenticators()拿到的是空列表,剛才所有的流程都沒有走,意味著APIView沒有任何的認證措施,那麼這些認證措施就需要我們自己來做了。。。。

 

如何自定義認證類?

怎麼做呢?缺什麼補什麼,在找authentication_classes的時候它會先去我們的檢視類中找,那麼我們就在檢視類中配置這麼個屬性,剛才也推導過了,它是個列表或者元祖,那麼我們就用列表好了,進一步反推,它會for迴圈這個列表並拿出一個個類.authenticate(),那麼我們就先寫一個類並實現authenticate方法,將類名放到該列表中,剛才我們推理得出,authenticate方法可以沒有返回值,也可以返回兩個值,一個是user物件,一個是auth物件,如果有返回值,它將user物件賦值給了request.user,這時候我們明白了,它的內部是在認證完了後將使用者物件塞給了request,此時的request就擁有了user這個全域性屬性

我順便去摟了一眼官方文件,我們需要自己定義認證類並且繼承BaseAuthentication,那麼接下來我們認證的寫程式碼:

 

 那麼至此,我們在用APIView的時候自定義認證就做完了,authenticate中的認證邏輯是可以根據自身公司和專案需要去做的,這裡是給出最簡單的示範。

當然,如果剛才你的思路一直走過來你會發現,我們除了在檢視類中配置authentication_classes以外還可以在配置檔案中配置,配置方法如下:

REST_FRAMEWORK={
                "DEFAULT_AUTHENTICATION_CLASSES":["app01.views.MyAuth",]
            }

如果這樣配置了的話,在全域性的檢視類都會生效,顯然對於網站註冊登陸主頁進行校驗使用者認證是不合理的,那我們需要怎麼做呢

由於原始碼中查詢authentication_classes的順序是先從檢視類找,檢視類沒有然後找配置檔案,那麼我們可以在不需要校驗的檢視類中定義

authentication_classes = []

在相應檢視類中將它配置為空列表即可。

下面我們來總結下用法:

1、定義認證類(專案中一般在應用裡單開py檔案存放)
2、在認證類中實現authenticate方法,實現具體的認證邏輯
3、認證通過返回user_obj和認證物件,這樣request就擁有了全域性的user,認證不給前端拋異常

4、配置
  -在檢視類中配置
  -在全域性配置,區域性禁用

 

剛才說了APIView除了有認證,還有許可權和頻率控制

許可權校驗

許可權校驗的原始碼和認證幾乎一模一樣的思路,用法一模一樣

也是需要自定義許可權類,實現has_perission()方法,程式碼如下:

from rest_framework.permissions import BasePermission   # 匯入BasePermission

class MyPermision(BasePermission):      # 繼承BasePermission
    message = '不是超級使用者,檢視不了'     # 自定義返回給前端的訪問錯誤資訊
    def has_permission(self,request,view):
        if request.user.user_type==1:
            return True
        else:
            return False


class Book(APIView):                     
    permission_classes = [MyPermision,]    # 檢視類配置
    def get(self,request):
        pass

也可以全域性配置,區域性禁用:

在配置檔案REST_FRAMEWORK中加上配置:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES":['app01.views.MyAuth',],
    "DEFAULT_PERMISSION_CLASSES" : ['app01.views.MyPermission',]  ## ---加上這一行即可
}

區域性禁用方式一樣是在檢視類中配置 

permission_classes = []

 

頻率控制

-使用:
        -第一步,寫一個頻率類,繼承SimpleRateThrottle
            from rest_framework.throttling import SimpleRateThrottle
            #重寫get_cache_key,返回self.get_ident(request)
            #一定要記住配置一個scop=字串
            class MyThrottle(SimpleRateThrottle):
                scope = 'lxx'      
                def get_cache_key(self, request, view):
                    return self.get_ident(request)    # 這裡是頻率控制的條件,是以訪問IP來控制還是以使用者ID控制都可以,暴露的配置介面,
                                 #這裡呼叫的父類get_ident
f方法
-第二步:在setting中配置 REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES':{ 'lxx':'3/m' # 這裡的lxx 即在自定義頻率類中定義的scope } } -區域性使用 -在檢視類中配置: throttle_classes=[MyThrottle,] -全域性使用 -在setting中配置 'DEFAULT_THROTTLE_CLASSES':['自己定義的頻率類'], -區域性禁用 throttle_classes=[]

 

我們來看原始碼:

還是從initial()這裡進

 

點進去看

 

 這段是頻率校驗的核心程式碼,self.get_throttles()走的是和上面認證、許可權一樣的路子

也是去自定義頻率類、然後配置檔案裡找相應頻率控制類並且直接加()例項化了

 

 現在每次for迴圈出來的 throttle 都是一個例項化的物件,

if not throttle.allow_request(request, self):

allow_request()從字面意思上我們也能判斷出它應該是校驗是否對本次訪問放行的,那麼它的返回值應該就是True或者False

那麼這句判斷語句的意思就是:如果頻率校驗不通過,那麼就走if塊內部程式碼

self.throttled(request, throttle.wait())

這句程式碼的意思就是針對  throttle.wait()得到的不同限制結果丟擲不同異常給前端使用者

點進去看原始碼人家也是這麼幹的

   def throttled(self, request, wait):
        """
        If request is throttled, determine what kind of exception to raise.
        """
        raise exceptions.Throttled(wait)

 

那麼頻率校驗不通過的情況我們知道了,我們就要關注它內部是如何實現對頻率的校驗的

進到allow_request裡面去看,由於我們自定義的頻率校驗類沒有定義該方法,那麼就向它的父類找

 

 

我們來看原始碼人家是怎麼做的

# settings配置
"""
 REST_FRAMEWORK = {
                
                     'DEFAULT_THROTTLE_RATES':{
                        'lxx':'3/m'     # 頻率配置   每分鐘3次
                     }
                 }   
""" 

#以下原始碼+個人註釋

def allow_request(self, request, view):
        if self.rate is None:   # self.rate 是 get_rate() 通過我們宣告的scope = 'lxx' 去拿到的配置檔案中頻率配置中頻率值 '3/m'  ,
                   # 需要我們在頻率控制類中宣告scope = 'lxx',也就是配置中字典的鍵,
                   # 鍵是通過反射scope拿到的,詳見 get_rate()原始碼
return True self.key = self.get_cache_key(request, view) # self是自定義頻率類的物件,那麼get_cache_key將優先從自定義頻率類找 # 得到的是頻率控制的依據,是使用者IP\ID或者其他資訊,由重寫get_cache_key確定 if self.key is None: return True self.history = self.cache.get(self.key, []) # 從快取中拿到當前使用者的訪問記錄 self.now = self.timer() # 獲取當前時間戳      #self.now 是當前執行時間,self.duration是訪問頻率分母,以上述例子為例 這裡就是60 while self.history and self.history[-1] <= self.now - self.duration: self.history.pop() # 將訪問列表超時的去掉 history =[第三次訪問時間,第二次訪問時間,第一次訪問時間] # self.num_requests 訪問頻率分子,設定 '3/m' 表示每分鐘3次,以上例子為例,這裡就是 3 if len(self.history) >= self.num_requests: return self.throttle_failure() # 返回訪問失敗資訊 return self.throttle_success() # 訪問成功

 

以上的self.duration 和self.num_requests在訪問時自定義頻率控制類例項化的時候,已經通過父類的__init__產生,原始碼如下:

這裡又進一步呼叫了self.parse_rate()方法,傳入了自定義頻率類中的scope屬性值 :'3/m', 那麼這裡我們大概就能猜到parse_rate()

幹了件什麼事兒,對!那就是拆分這個字串,並返回  限制次數,限制時間長度,比如  3次,60秒

 一起來看parse_rate()原始碼:

    def parse_rate(self, rate):
        """
        Given the request rate string, return a two tuple of:
        <allowed number of requests>, <period of time in seconds>
        """
        if rate is None:
            return (None, None)

        # 對 '3/m' 進行切分  num_requests=3,duration = 60
        num, period = rate.split('/')
        num_requests = int(num)
        duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]  # period[0]取到切分後字串 'm' 的第一個元素
        return (num_requests, duration)   # (3, 60)

 

 以上allow_request()如果校驗不通過,那麼直接呼叫 throttle.failure()直接返回false

如果校驗通過,它應該還需要將本次訪問新增到訪問記錄中並儲存了,那麼我們猜 throttle.success() 幹了這麼件事

來看原始碼發現果然幹了這麼件事兒!

    def throttle_success(self):
        """
        Inserts the current request's timestamp along with the key
        into the cache.
        """
        self.history.insert(0, self.now)   # 將當前訪問時間插入 訪問記錄第一個位置
        self.cache.set(self.key, self.history, self.duration)  # 更新訪問資訊記錄快取
        return True   # 返回訪問成功 True 供判斷

 

 至此,頻率控制部分的原始碼我們也學習完了,當我們回過頭去看用法的時候會有種豁然開朗的感覺!

 

看原始碼小技巧:

1、只看自己看得懂的

2、屬性查詢一定要縷清繼承關係,明確當前的self是誰的物件,然後從它開始查詢

3、**kwargs,*args相關一般不看

 

相關文章