django-rest-framework-原始碼解析004-三大驗證(認證/許可權/限流)

GCX發表於2020-07-23

三大驗證模組概述

在DRF的APIView重寫的dispatch方法中,  self.initial(request, *args, **kwargs) 這句話就是執行三大驗證的邏輯, 點進去可以看到依次執行的就是認證(authentication)/許可權(permission)/限流(throttle)

        # Ensure that the incoming request is permitted
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)

簡單來說認證的作用就是驗證傳入請求中攜帶的使用者資訊

如果使用者資訊沒有問題, 那麼就進行許可權驗證, 也就是判斷這個使用者是否有權訪問這個頁面

如果也有權訪問, 那麼就進行限流(頻率)驗證, 也就是判斷這個使用者是不是訪問該頁面是否過於頻繁(這是一個反爬策略)

認證模組

有些頁面是需要使用者登入才能訪問的, 而因為http是無狀態的, 無法直接在請求中攜帶使用者的登入狀態, 因此如何判斷使用者是否登入的一般方法就是在使用者登入後, 後臺生成並記錄一個token資訊, 然後將token資訊返回給瀏覽器, 下次訪問需要登入才能訪問的頁面時, 就在請求中帶上這個token資訊, 後臺拿到這個token資訊後, 經過比對校驗或者解密校驗, 拿到了對應的使用者資訊, 那麼就說明這個token攜帶的使用者是沒有問題的, 就可以訪問這個頁面或者繼續進行後續的校驗了.

原始碼流程分析

我們點開上面認證模組的入口函式 self.perform_authentication(request) , 可以看到裡面只是簡單一句話 request.user , 這有點像獲取request的user屬性, 前面第一章已經知道了, 這個request是DRF封裝好的一個request, 於是我們來到rest_framework.request.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

可以看到這是一個property裝飾的方法, 難怪直接.user就可以執行了, 可以看到該方法返回的是self的_user屬性, 前面我們並沒有給該屬性賦值, 根據上面的if條件我們可以看出一開始request並沒有_user屬性, 於是就說明在 self._authenticate() 中應該有賦值的邏輯

我們再回顧一下在上一個函式的 self.perform_authentication(request) 中只是簡單呼叫了一下 request.user , 而並沒有將 request.user 的值返回出去, 說明 self.perform_authentication(request) 並不想要返回什麼, 而只是想讓程式執行 self._authenticate() 這個方法而已

我們再點進去, 來到 _authenticate() 方法:

    def _authenticate(self):
        """
        Attempt to authenticate the request using each authentication instance
        in turn.
        """
        # 遍歷驗證器
        for authenticator in self.authenticators:
            try:
                # 執行驗證器的 authenticate 方法, 返回驗證的結果, 該結果是一個元組 (user, auth)
                user_auth_tuple = authenticator.authenticate(self)
            except exceptions.APIException:
                # 如果驗證器的 authenticate 方法丟擲 APIException 異常,
                # 說明驗證程式碼邏輯可能出現了異常情況
                # 那麼就執行 _not_authenticated 方法
                self._not_authenticated()
                # 執行完後還要繼續向外丟擲異常
                raise

            # 如果驗證器的 authenticate 方法正常返回了元組, 就給request物件新增三個屬性: _authenticator user auth
            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple
                return

        # 否則繼續執行 _not_authenticated 方法
        self._not_authenticated()

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

        Defaults are None, AnonymousUser & None.
        """
        # 該方法說明請求沒有認證資訊, 那麼就將:
        # _authenticator 屬性設定None,
        # user 屬性設為 api_settings.UNAUTHENTICATED_USER()的值, 這個值可以在我們的settings檔案中自己設定,
        #      預設值為Django預定義的匿名使用者物件AnonymousUser
        # auth 屬性設為 api_settings.UNAUTHENTICATED_TOKEN()的值, 這個值也可以在我們的settings檔案中自己設定,
        #      預設值為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()方法可能返回兩種結果:

  第一種結果是遍歷到某個認證器時, 其authenticate(self)返回了一個非空(user, auth)元組, 那麼就說明認證成功

  第二種結果是認證器執行異常或者返回空元組, 那麼就說明該請求沒有攜帶認證資訊, 預設返回的就是django的匿名使用者, 這個返回值可以在我們自己的settings中自定義

那麼說明詳細的認證校驗邏輯還是在認證器的authenticate()方法中, 根據前面的經驗我們知道DRF的各種器(如前面說的解析器和渲染器)一般都會組成一個物件列表, 然後迴圈呼叫物件的函式, 這裡的認證器同樣如此, 上面程式碼告訴我們request有一個 authenticators 屬性, 這個屬性應該是一個認證器列表, 但是我們分析過程中並沒有發現給這個屬性賦值的邏輯, 回憶一下發現其實這個賦值邏輯是在dispatch的三大驗證的上一步, 初始化Request物件時執行的, 進入到dispatch的 request = self.initialize_request(request, *args, **kwargs) 中, 可以看到賦值語句 authenticators=self.get_authenticators() 再點進去就可以找到典型的DRF程式碼

    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可以看到DRF預設的認證器有兩個 SessionAuthentication和BasicAuthentication

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

也就是說預設情況下, 我們每個DRF請求都會經過這兩個驗證器, 那為什麼沒有驗證報錯呢?我們明明都沒有給驗證資訊. 我們找 BasicAuthentication 看看其原始碼一探究竟

BasicAuthentication

我們來到rest_framework.authentication.py. 可以看到DRF預定義了幾個認證器, 如 BasicAuthentication/ RemoteUserAuthentication/ SessionAuthentication/ TokenAuthentication, 我們分析一下 BasicAuthentication 原始碼, 有助於我們自己寫自定義的認證器

驗證規則

這裡我們先說一下驗證規則, 首先, 既然是驗證, 那麼一定需要預先定義一個驗證的規則才能判斷驗證是否通過, BasicAuth 驗證的規則就是給請求頭的 Authorization 引數新增一個字串, 這個字串的組成規則就是'basic+空格+金鑰', basic是固定的字串字首, 可以理解為是鹽值.金鑰是使用者名稱和密碼組成的字串在進行加密得到的.

在程式驗證時, 首先拿到 Authorization的值, 然後判斷該值組成格式是否符合前面定的規則, 不符合則返回None, 符合則解析金鑰, 得到使用者名稱和密碼, 然後和資料庫的使用者名稱密碼進行校驗, 不通過拋異常, 通過則說明整個驗證流程通過

上面的這個規則是BasicAuth的驗證規則, 應該是比較常見的驗證規則之一, 當然驗證規則隨著時間的推移, 越常見可能就越容易被攻破, 於是也有一些其他的通用驗證規則誕生, 我們可以在postman軟體上選擇常見的驗證規則, 初次之外, postman還能將我們輸入的使用者名稱密碼進行加密再傳送出去, 這樣能夠省去我們手動去加密, 非常好用

 原始碼分析

前面我們知道驗證器的核心程式就是 authenticate() 方法, 該方法在自定義認證器時是必須要寫的.

class BasicAuthentication(BaseAuthentication):
    """
    HTTP Basic authentication against username/password.
    """
    www_authenticate_realm = 'api'

    def authenticate(self, request):
        """
        Returns a `User` if a correct username and password have been supplied
        using HTTP Basic authentication.  Otherwise returns `None`.
        """
        # 將請求頭的 Authorization 引數值按空格拆分 成一個列表
        auth = get_authorization_header(request).split()

        # 若拆分的列表為空或者第一位不是basic, 則返回None
        if not auth or auth[0].lower() != b'basic':
            return None

        # 若auth長度!=2則丟擲異常
        if len(auth) == 1:
            msg = _('Invalid basic header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid basic header. Credentials string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        # 解密得到使用者名稱和密碼
        try:
            auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).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 get_authorization_header(request):
        """
        Return request's 'Authorization:' header, as a bytestring.

        Hide some test client ickyness where the header can be unicode.
        """
        # 獲取請求頭中的Authorization引數值            
        #   在DRF中因為request已經是被封裝過的, 因此獲取請求頭資料時, 可以使用原生的request._request.headers.get()
        #   也可以使用request.META.get(), 但是META中對原請求引數進行了改造:
        #      a) 將原中劃線(-)改成了下劃線(_)
        #      b) 將字母全都轉成大寫
        #      c) 原引數前都加上了HTTP_字首
        auth = request.META.get('HTTP_AUTHORIZATION', b'')
        # 將auth編碼為 iso-8859-1
        if isinstance(auth, str):
            # Work around django test client oddness
            auth = auth.encode(HTTP_HEADER_ENCODING)
        return auth

    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物件
        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.'))

        # 成功的到User物件, 則返回元組
        return (user, None)

這裡看原始碼我們可以發現, 它有三種返回結果:

    a. None : 當 Authorization 引數值第一位不是basic時返回, 結合上一步的外層流程 _authenticate() , 這時最終會認為沒有攜帶認證資訊, 返回匿名使用者 AnonymousUser

    b. raise exceptions.AuthenticationFailed : 大部分認證失敗情況時丟擲, 上一步的外層流程 _authenticate() 並不會將該異常捕獲, 繼續往外丟擲, 最終在dispatch中被捕獲了, 交給異常模組進行返回處理

    c. return (user, None) : 認證通過時返回, 必須返回一個元組, 結合上一步的外層流程 _authenticate() , 會給request賦值三個屬性

自定義驗證類

我們可以仿照上述BasicAuthentication自定義一個驗證類叫MyAuthentication , 規則為先進行 BasicAuthentication 驗證, 若驗證未通過, 還可以進行我們的MyAuthentication驗證, 我們的驗證規則為, 在請求頭中傳一個my-token引數, 字串格式為'auth_sort/username', 若傳入格式正確且username存在與資料庫中, 則認證通過, 程式碼示例為:

class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        """
        約定在請求頭中帶一個my-token引數, 格式為 auth_sort/username
        注意:
            1. 在HTTP請求頭中的引數最好不要使用下劃線, 否則有可能獲取不到這個引數
            2. 在DRF中因為request已經是被封裝過的, 因此獲取請求頭資料時, 可以使用原生的request._request.headers.get()
               也可以使用request.META.get(), 但是META中對原請求引數進行了改造:
                    a) 將原中劃線(-)改成了下劃線(_)
                    b) 將字母全都轉成大寫
                    c) 原引數前都加上了HTTP_字首
                那麼原引數 my-token 就變成了 HTTP_MY_TOKEN
        """
        # 獲取token引數
        token = request.META.get('HTTP_MY_TOKEN')
        # print(request._request.headers.get('my-token'))
        if not token:
            raise exceptions.AuthenticationFailed('無token, 認證失敗')
        # 拆分token
        auth = token.split('/')
        if len(auth) != 2 or auth[0] != 'auth_sort':
            raise exceptions.AuthenticationFailed('token格式錯誤')
        # 拿到使用者名稱判斷使用者是否存在
        user = User.objects.filter(username=auth[1])
        if not user:
            raise exceptions.AuthenticationFailed('使用者名稱不存在')
        # 認證成功
        return user, None


class BookAuthViewSet(mixins.ListModelMixin, GenericViewSet):
    queryset = Book.objects.filter(is_delete=False)
    serializer_class = BookSerializer
    authentication_classes = [BasicAuthentication, MyAuthentication, ]

    def list(self, request, *args, **kwargs):
        print(request.user)
        response = super().list(request, *args, **kwargs)
        return MyResponse(response.data)

許可權模組

程式碼分析方式與認證模組相似, 點進 self.check_permissions(request) 可以看到 self.get_permissions() 返回了一個許可權器列表, 迴圈許可權器的 has_permission(request, self) 方法返回是否有許可權訪問, 如果沒有許可權訪問, 則丟擲異常 raise exceptions.PermissionDenied(detail=message) , 來到rest_framework.settings檢視預設的許可權類為 AllowAny

'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]

來到 rest_framework.permissions.AllowAny類, 可以看到其 has_permission 只是簡單的返回了True, 也就是顧名思義, 任何請求進來都允許通過

class AllowAny(BasePermission):
    """
    Allow any access.
    This isn't strictly required, since you could use an empty
    permission_classes list, but it's useful because it makes the intention
    more explicit.
    """

    def has_permission(self, request, view):
        return True

需要注意的是許可權類除了 has_permission(self, request, view) 外, 還可以定義 has_object_permission(self, request, view, obj) , 這個許可權驗證方法針對的是具體某一個模型物件, 而 has_permission 是針對檢視類的許可權限制.

DRF還定義了其他許可權類, 如 IsAdminUser(user.is_staff必須為True) / IsAuthenticated(經過了認證的使用者) / IsAuthenticatedOrReadOnly(請求方式為GET/HEAD/OPTIONS或者經過了認證的使用者)

如果許可權驗證失敗, 可以將錯誤資訊定義在許可權類的類屬性message中, 在dispatch的 check_permissions(self, request) 方法中, 如果 permission.has_permission 返回為False, 則 self.permission_denied(request, message=getattr(permission, 'message', None)) 會獲取許可權類的 message 屬性作為報錯資訊.

自定義許可權類

要自定義許可權,需要繼承 BasePermission 類,並實現以下方法中的一個或兩個:

  has_permission(self, request, view)

  has_object_permission(self, request, view, obj)

如果請求被授予訪問許可權,方法應該返回 True ,否則返回 False 。

這裡我們定義一個許可權規則就是: 匿名使用者不能檢視book資料, 普通使用者只能檢視id為1的出版社出版的圖書的詳細資訊, 管理員使用者可以檢視所有資料

class MyPermission(BasePermission):
    """
    匿名使用者不能檢視book資料, 普通使用者只能檢視id為1的出版社出版的圖書的詳細資訊, 管理員使用者可以檢視所有資料
    """
    message = '非管理員使用者只能檢視上海出版社的資料'

    def has_permission(self, request, view):
        # 匿名使用者返回False
        return not isinstance(request.user, AnonymousUser)

    def has_object_permission(self, request, view, obj):
        # 管理員或者非管理員且圖書出版社ID為1, 返回True
        return bool(request.user.is_staff or (request.user.is_staff is False and obj.publish_id == 1))


class BookAuthViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
    queryset = Book.objects.filter(is_delete=False)
    serializer_class = BookSerializer
    authentication_classes = [BasicAuthentication, MyAuthentication]
    permission_classes = [MyPermission, ]

    def list(self, request, *args, **kwargs):
        print(request.user)
        response = super().list(request, *args, **kwargs)
        return MyResponse(result=response.data)

限流模組(或頻率模組)

限流可以形象地比喻為節流閥,指示 一種臨時狀態,用於控制客戶端在某段時間內允許向API發出請求的次數,也就是頻率。例如,你可能希望將使用者限制為每分鐘最多60個請求,每天1000個請求。

程式碼分析方式與認證模組相似, 點進  self.check_throttles(request)  可以看到  self.get_throttles()  返回了一個限流器列表, 迴圈限流器的 allow_request(request, self) 方法返回是否允許訪問, 如果不允許訪問, 則呼叫限流器的wait()方法返回還需等待的時間, 將每個限流器的還需等待時間存放在 throttle_durations 陣列中, 找出最大的時間, 丟擲異常 raise exceptions.Throttled(wait) 

    def check_throttles(self, request):
        """
        Check if request should be throttled.
        Raises an appropriate exception if the request is throttled.
        """
        throttle_durations = []
        for throttle in self.get_throttles():
            if not throttle.allow_request(request, self):
                throttle_durations.append(throttle.wait())

        if throttle_durations:
            # Filter out `None` values which may happen in case of config / rate
            # changes, see #1438
            durations = [
                duration for duration in throttle_durations
                if duration is not None
            ]

            duration = max(durations, default=None)
            self.throttled(request, duration)

來到settings中可以看到預設的限流器是空列表, 說明不進行限流.

來到rest_framework.throttling.py可以看到一些定義好的限流器, 如  SimpleRateThrottle(BaseThrottle) / AnonRateThrottle(SimpleRateThrottle) / UserRateThrottle(SimpleRateThrottle) / ScopedRateThrottle(SimpleRateThrottle) 

通過繼承關係我們可以發現  SimpleRateThrottle  是其他限流器的父類, 前面提到的限流器必須有的方法  allow_request() 和 wait()  都是在 SimpleRateThrottle 類中定義的, 而其他子類只是重寫了  get_cache_key(self, request, view) 這個方法. 那我們就來看看 SimpleRateThrottle   的原始碼, 有益於幫助我們寫自定義的限流器

SimpleRateThrottle原始碼分析

通過類的文件字串可以看到: 這是一個通過快取實現的簡單頻率限制器, 頻率需要設定為 '請求的次數/週期' 這樣的格式, 比如一秒鐘3次則為'3/s' , 這裡我們以一分鐘三次('3/m')為例講解程式碼

我們直接來到 allow_request() 方法, 這是程式的入口.

    def allow_request(self, request, view):
        """
        Implement the check to see if the request should be throttled.

        On success calls `throttle_success`.
        On failure calls `throttle_failure`.
        """
        # 未設定頻率, 則允許請求通過
        if self.rate is None:
            return True

        # 拿到快取的key, 若key為空, 則允許請求通過
        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        # 拿到快取key對應的快取值
        self.history = self.cache.get(self.key, [])
        # 記錄當前時間
        self.now = self.timer()
     ......程式碼省略

可以看到需要呼叫 get_cache_key 獲取快取的key, 再通過key獲取快取的值, 那麼我們先來看看這個快取的key和值到底存的是什麼

來到上面的 get_cache_key 發現該方法需要在子類中重寫, 那麼我們就看一下子類 AnonRateThrottle(SimpleRateThrottle) 是怎麼重寫這個方法的.  AnonRateThrottle 針對的就是對匿名使用者的限流

class AnonRateThrottle(SimpleRateThrottle):
    """
    Limits the rate of API calls that may be made by a anonymous users.

    The IP address of the request will be used as the unique cache key.
    """
    # 定義一個字串字首
    scope = 'anon'

    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            return None  # Only throttle unauthenticated requests.

        # 給cache_format字串賦值
        return self.cache_format % {
            'scope': self.scope,
            'ident': self.get_ident(request)
        }

可以看到其實 get_cache_key 方法就是給 cache_format 屬性賦了值並返回, 點進去可以看到 cache_format 字串的格式為  cache_format = 'throttle_%(scope)s_%(ident)s' ,

這是字串格式化%的一種鍵值對賦值方法, 一般我們使用%格式化時, 都是在後面的小括號中按順序寫出需要替代的值是什麼, 而這種鍵值對賦值方式語法為在格式化的字串中把變數名用小括號包裹, 然後在%後面使用大括號字典的方式通過鍵值對賦值. 上述賦值語句中 get_ident(request) 返回的是通過IP地址等請求資訊確定的該匿名使用者的唯一標識

因為限流限制的是某一個使用者或者某一臺機器的訪問頻率, 而匿名使用者有沒有對應的使用者ID, 所以只能通過IP等資訊去確定唯一性, 我們不妨再看看另外一個限流器 UserRateThrottle(SimpleRateThrottle) 重寫的 get_cache_key , 可以看到其字首是 'user', 'ident'是使用者的主鍵pk, 

class UserRateThrottle(SimpleRateThrottle):
    """
    Limits the rate of API calls that may be made by a given user.

    The user id will be used as a unique cache key if the user is
    authenticated.  For anonymous requests, the IP address of the request will
    be used.
    """
    scope = 'user'

    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            ident = request.user.pk
        else:
            ident = self.get_ident(request)

        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }

通過上面兩個例子可以看到 get_cache_key  返回的就是一個標識使用者唯一性的字串, 最終 AnonRateThrottle 的返回值就是  throttle_anons_123jnajduyyu12h3bs  ,  UserRateThrottle 的返回值就是  throttle_users_1s 

弄清楚了 get_cache_key , 我們再回到 SimpleRateThrottle 的 allow_request() 

    def allow_request(self, request, view):
        """
        Implement the check to see if the request should be throttled.

        On success calls `throttle_success`.
        On failure calls `throttle_failure`.
        """
        # 未設定頻率, 則允許請求通過
        if self.rate is None:
            return True

        # 拿到快取的key, 若key為空, 則允許請求通過
        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        # 拿到快取key對應的快取值, 第一次拿到的是一個空列表
        self.history = self.cache.get(self.key, [])
        # 記錄當前時間
        self.now = self.timer()

        # Drop any requests from the history which have now passed the
        # throttle duration
        # 迴圈history列表, 當列表最後一個元素小於當前時間減去過期時間, 就把最後一個元素彈出
     #
即當前時間減去最早的history時間大於等於過期時間, 說明最早的history時間已經超過了頻率限制的時間, 就把該時間彈出, 不記為限制的次數
        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()
        # 如果history長度大於等於限制的請求次數, 則說明請求次數太多了, 沒有通過頻率驗證
        if len(self.history) >= self.num_requests:
            return self.throttle_failure()
        # 否則通過了頻率驗證, 允許請求
        return self.throttle_success()

前面我們拿到了 self.key 的值, 下一步是在快取中拿到key對應的值並賦值給history屬性, 若拿不到, 則返回一個空列表, 說明這個history應該是一個記錄某個歷史的列表, 我們前面分析步驟是模擬的第一次請求的步驟, 在前面的步驟中並沒有給快取插入資料的邏輯, 那麼現在拿到的history應該就是一個空列表

既然是空列表那麼就不會進入while迴圈, history列表長度應該也不會>=self.num_requests , 除非 self.num_requests 也等於0, 我們可以看到 self.duration 和 self.num_requests 在前面並沒有賦值過, 那麼點進去看看這兩個屬性到底是什麼意思

點進去發現這兩個引數是在__init__初始化時 self.num_requests, self.duration = self.parse_rate(self.rate) 賦值的

先看一下 self.rate 屬性是如何得到的, 發現是通過 get_rate(self) 獲取的, 點進去發現其實際上是一個類屬性 THROTTLE_RATES , 這個類屬性可以在我們自定義的限流器中定義(即區域性定義), 也可以在專案的settings中定義(即全域性定義), 預設的定義為

# Throttling
    'DEFAULT_THROTTLE_RATES': {
        'user': None,
        'anon': None,
    }

然後回到上一步的 parse_rate(self.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)
        num, period = rate.split('/')
        num_requests = int(num)
        duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
        return (num_requests, duration)

發現其就是把傳進來的rate進行'/'拆分然後解析的, 如rate='3/m', 說明一分鐘最多訪問三次, 那麼解析出來的 num_requests=3 , duration=60, 說明最終都是轉換為xxx秒最多訪問xxx次, 若num_requests=0, 就說明一次都不能訪問, 通通拒絕.

弄清了這兩個屬性後, 繼續回到 allow_request() , 發現只要num_requests不等於0, 那麼第一次請求是肯定能夠通過的, 就來到了 throttle_success() 方法

    def throttle_success(self):
        """
        Inserts the current request's timestamp along with the key
        into the cache.
        """
        # 在history列表的第一位插入當前的時間
        self.history.insert(0, self.now)
        # 在快取中也設定一條資料, 鍵為get_cache_key返回的key, 值為history列表, 過期時間為rate設定的xxx秒
        self.cache.set(self.key, self.history, self.duration)
        return True

這個方法做了兩件事, 第一件就是往history列表頭中插入了當前時間, 第二件事就是往快取中插入了history列表, 並設定了過期時間.例如, 此時 history=['2020-07-23 10:30:00'] 

我們這裡以一分鐘最多訪問三次為例, 那麼:

        當我們在一分鐘之內(如 '2020-07-23 10:30:10' )第二次訪問該介面時,  len(history) = 1  , 驗證依然通過, 此時 history=['2020-07-23 10:30:10', '2020-07-23 10:30:00']  , 然後會重新設定快取, 快取過期時間為新的60秒

        當我們在一分鐘之內(如 '2020-07-23 10:30:20' )第三次訪問該介面時  len(history) = 2 , 驗證依然通過, 此時 history=['2020-07-23 10:30:20', '2020-07-23 10:30:10', '2020-07-23 10:30:00'] , 然後會重新設定快取, 快取過期時間為新的60秒

        當我們在一分鐘之內(如 '2020-07-23 10:30:30' )第四次訪問該介面時   len(history) = 3  , len(self.history) >= self.num_requests 即 3>=3 成立, 驗證不通過, 返回False

此時程式回到上一步外層的 check_throttles(self, request) , 將呼叫限流器的 wait() 方法, 把該方法返回的需要等待時間追加到列表 throttle_durations 中.

於是來到 wait() 方法 , 該方法就是計算出還需要等待多長時間才能再次訪問該介面

    def wait(self):
        """
        Returns the recommended next request time in seconds.
        """
        if self.history:
            # 下一次訪問需要等待的時間=過期時間-(當前時間-最早的history時間)
            remaining_duration = self.duration - (self.now - self.history[-1])
        else:
            remaining_duration = self.duration

        # 剩餘可訪問的次數=限制總次數-已訪問的次數, 按實際情況算這個值結果應該都為1
        available_requests = self.num_requests - len(self.history) + 1
        if available_requests <= 0:
            return None

        # 返回需要等待的時間
        return remaining_duration / float(available_requests)

再次回到 check_throttles(self, request) , 得到了wait()的時間之後, 找到限流器中需要等待時間最長的時間 duration = max(durations, default=None) , 然後丟擲異常, 提示還需等待多少秒才能繼續訪問

這時我們的案例應該返回的是還需要30秒才能繼續訪問, 

當時間走到  ''2020-07-23 10:31:00''  時, 我們再次訪問該介面, 此時  history=['2020-07-23 10:30:20', '2020-07-23 10:30:10', '2020-07-23 10:30:00'] , 那麼 while self.history and self.history[-1] <= self.now - self.duration: 這句話就會成立, 即最早的history時間已經過了60秒到期時間, 那麼就會執行 self.history.pop() ,把最在的history時間彈出, 這樣 history=['2020-07-23 10:30:20', '2020-07-23 10:30:10'] 

程式繼續往下走, 此時 if len(self.history) >= self.num_requests 即 2>=3 不成立, 那麼就驗證通過, 走到 throttle_success() , 繼續往history和快取中插入資料, 此時 history=['2020-07-23 10:31:00', '2020-07-23 10:30:20', '2020-07-23 10:30:10'] 

如果又立馬訪問該介面, 那麼又會丟擲異常, wait時間為10秒

許可權類測試

我們在檢視類中區域性定義許可權類設定, 普通使用者一分鐘最多訪問5次, 匿名使用者一分鐘最多訪問3次

class BookAuthViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
    # 查詢結果集
    queryset = Book.objects.filter(is_delete=False)
    # 序列化類
    serializer_class = BookSerializer
    # 認證類列表
    authentication_classes = [BasicAuthentication, MyAuthentication]
    # 許可權類列表
    permission_classes = [MyPermission, ]
    # 限流/頻率類列表
    throttle_classes = [UserRateThrottle, AnonRateThrottle]

RATE在專案的settings.py中設定

REST_FRAMEWORK = {
    # 限流頻率
    'DEFAULT_THROTTLE_RATES': {
        'user': '5/m',
        'anon': '3/m',
    }
}

測試結果為:

 

相關文章