先捋一下請求到APIview的過程:api
as_view-->dispatch -->initialize_request-->initial-->perform_authentication-->check_permissions-->check_throttles(就是在這裏實現了頻率限制)緩存
1 def initial(self, request, *args, **kwargs): 2 """ 3 Runs anything that needs to occur prior to calling the method handler. 4 """ 5 self.format_kwarg = self.get_format_suffix(**kwargs) 6 7 # Perform content negotiation and store the accepted info on the request 8 neg = self.perform_content_negotiation(request) 9 request.accepted_renderer, request.accepted_media_type = neg 10 11 # Determine the API version, if versioning is in use. 12 version, scheme = self.determine_version(request, *args, **kwargs) 13 request.version, request.versioning_scheme = version, scheme 14 15 # Ensure that the incoming request is permitted 16 17 # 身份驗證 18 self.perform_authentication(request) 19 # 權限驗證 20 self.check_permissions(request) 21 # 訪問頻率限制 22 self.check_throttles(request)
那check_throttles到底作了什麼呢?app
1 def check_throttles(self, request): 2 """ 3 Check if request should be throttled. 4 Raises an appropriate exception if the request is throttled. 5 """ 6 for throttle in self.get_throttles(): 7 if not throttle.allow_request(request, self): 8 self.throttled(request, throttle.wait())
其實和check_permissions很類似,分爲如下幾個步驟:ide
1. self.get_throttles() 經過列表推導式拿到了註冊的throttle類,並將其實例化返回測試
1 def get_throttles(self): 2 """ 3 Instantiates and returns the list of throttles that this view uses. 4 """ 5 return [throttle() for throttle in self.throttle_classes]
2. throttle.allow_request說明throttle類中必定要實現allow_request方法,而且返回值爲True表示正確容許訪問,就執行下次循環,檢查下一個頻率控制對象ui
按照以前的套路,throttle組件中應該有個基礎的throttle類,找一下:this
1 class BaseThrottle(object): 2 """ 3 Rate throttling of requests. 4 """ 5 6 def allow_request(self, request, view): 7 """ 8 Return `True` if the request should be allowed, `False` otherwise. 9 """ 10 raise NotImplementedError('.allow_request() must be overridden') 11 12 def get_ident(self, request): 13 """ 14 Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR 15 if present and number of proxies is > 0. If not use all of 16 HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR. 17 """ 18 xff = request.META.get('HTTP_X_FORWARDED_FOR') 19 remote_addr = request.META.get('REMOTE_ADDR') 20 num_proxies = api_settings.NUM_PROXIES 21 22 if num_proxies is not None: 23 if num_proxies == 0 or xff is None: 24 return remote_addr 25 addrs = xff.split(',') 26 client_addr = addrs[-min(num_proxies, len(addrs))] 27 return client_addr.strip() 28 29 return ''.join(xff.split()) if xff else remote_addr 30 31 def wait(self): 32 """ 33 Optionally, return a recommended number of seconds to wait before 34 the next request. 35 """ 36 return None
3. 若是返回值爲False,就執行 self.throttledspa
# 拋出Throttled異常
1 def throttled(self, request, wait): 2 """ 3 If request is throttled, determine what kind of exception to raise. 4 """ 5 raise exceptions.Throttled(wait)
# Throttled異常類
1 class Throttled(APIException): 2 status_code = status.HTTP_429_TOO_MANY_REQUESTS 3 default_detail = _('Request was throttled.') 4 extra_detail_singular = 'Expected available in {wait} second.' 5 extra_detail_plural = 'Expected available in {wait} seconds.' 6 default_code = 'throttled' 7 8 def __init__(self, wait=None, detail=None, code=None): 9 if detail is None: 10 detail = force_text(self.default_detail) 11 if wait is not None: 12 wait = math.ceil(wait) 13 detail = ' '.join(( 14 detail, 15 force_text(ungettext(self.extra_detail_singular.format(wait=wait), 16 self.extra_detail_plural.format(wait=wait), 17 wait)))) 18 self.wait = wait 19 super(Throttled, self).__init__(detail, code)
通常來講,接口若是不作登陸限制,那就會容許匿名用戶和已登陸用戶都能訪問。因此這個接口就要考慮能對匿名用戶和登陸用戶都進行訪問頻率限制:代理
思路:rest
已經登陸用戶能夠根據身份作判斷,固定時間內,同一個用戶的身份只能訪問限定次數
未登陸用戶可經過IP地址判斷,對同一個IP的請求進行限制
1 class MyThrottle(BaseThrottle): 2 ctime = time.time 3 4 def get_ident(self, request): 5 6 """ 7 reuqets.user有值,不是匿名用戶 8 根據用戶IP和代理IP,當作請求者的惟一IP 9 Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR 10 if present and number of proxies is > 0. If not use all of 11 HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR. 12 """ 13 14 user = request.user 15 if user: 16 # 有用戶身份,直接返回用戶 17 return user 18 19 xff = request.META.get('HTTP_X_FORWARDED_FOR') 20 remote_addr = request.META.get('REMOTE_ADDR') 21 num_proxies = api_settings.NUM_PROXIES 22 23 if num_proxies is not None: 24 if num_proxies == 0 or xff is None: 25 return remote_addr 26 addrs = xff.split(',') 27 client_addr = addrs[-min(num_proxies, len(addrs))] 28 return client_addr.strip() 29 30 return ''.join(xff.split()) if xff else remote_addr 31 32 def allow_request(self, request, view): 33 """ 34 是否仍然在容許範圍內 35 Return `True` if the request should be allowed, `False` otherwise. 36 :param request: 37 :param view: 38 :return: True,表示能夠經過;False表示已超過限制,不容許訪問 39 """ 40 # 獲取用戶惟一標識(如:IP) 41 42 # 容許一分鐘訪問10次 43 num_request = 10 44 time_request = 60 45 46 now = self.ctime() 47 ident = self.get_ident(request) 48 self.ident = ident 49 if ident not in RECORD: 50 RECORD[ident] = [now, ] 51 return True 52 history = RECORD[ident] 53 while history and history[-1] <= now - time_request: 54 history.pop() 55 if len(history) < num_request: 56 history.insert(0, now) 57 return True 58 59 def wait(self): 60 """ 61 多少秒後能夠容許繼續訪問 62 Optionally, return a recommended number of seconds to wait before 63 the next request. 64 """ 65 last_time = RECORD[self.ident][0] 66 now = self.ctime() 67 return int(60 + last_time - now)
1 class MemberPrograms(APIView): 2 throttle_classes = [MyThrottle, ] 3 4 def get(self, request): 5 programs = MemberProgram.objects.all().values() 6 return JsonResponse(list(programs), safe=False)
測試:
其實restframework已經幫咱們實現了一些簡單的頻率限制類 咱們只須要稍加修改,好比SimpleRateThrottle
class SimpleRateThrottle(BaseThrottle):
""" A simple cache implementation, that only requires `.get_cache_key()` to be overridden. The rate (requests / seconds) is set by a `throttle` attribute on the View class. The attribute is a string of the form 'number_of_requests/period'. Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day') Previous request information used for throttling is stored in the cache. """ cache = default_cache timer = time.time cache_format = 'throttle_%(scope)s_%(ident)s' scope = None # 頻率的key名 # 必須設置 THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES # {scope:rate} 好比:{‘scopre’:10/m} def __init__(self): if not getattr(self, 'rate', None): self.rate = self.get_rate() self.num_requests, self.duration = self.parse_rate(self.rate) def get_cache_key(self, request, view): """ 必須重寫,返回一個惟一的身份值做爲緩存的key Should return a unique cache-key which can be used for throttling. Must be overridden. May return `None` if the request should not be throttled. """ raise NotImplementedError('.get_cache_key() must be overridden') def get_rate(self): """ Determine the string representation of the allowed request rate. """ if not getattr(self, 'scope', None): # 那不到scope就拋出異常 msg = ("You must set either `.scope` or `.rate` for '%s' throttle" % self.__class__.__name__) raise ImproperlyConfigured(msg) try: # 從{scope:rate}中嘗試取rate return self.THROTTLE_RATES[self.scope] except KeyError: # 取不到就拋出異常,因此rate也必須設置 msg = "No default throttle rate set for '%s' scope" % self.scope raise ImproperlyConfigured(msg) 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) 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 self.key = self.get_cache_key(request, view) if self.key is None:
# 沒有key,說明沒有訪問記錄,容許訪問 return True
# 獲取歷史請求時間列表 self.history = self.cache.get(self.key, [])
獲取當前時間 self.now = self.timer() # Drop any requests from the history which have now passed the # throttle duration
# 若是歷史訪問時間列表有記錄,而且列表記錄中最先的訪問時間小於當前時間-限制時間,說明已通過了限制時間
# 例如,假設請求都在同一分鐘內比較容易理解:list = [4,10,23] 表示在第4,10,25秒分表訪問了一次
# 當前時間56秒 限制是20秒內3次:
# 56-20 = 25 只要列表最後一個元素小於25,那說明已通過了限制時間,距離最先的一次訪問,就刪除列表中的23: [4,10]
# 持續循環檢查,直到[]爲空,或者此次請求距離列表最先的請求小於頻率限制時間
while self.history and self.history[-1] <= self.now - self.duration: # 刪除掉這條記錄,pop()刪除列表最後一個元素
self.history.pop()
# 若是列表中的訪問時間記錄次數等於限制次數,說明沒有被pop掉的記錄,訪問請求失敗,不然請求成功 if len(self.history) >= self.num_requests: return self.throttle_failure() return self.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 def throttle_failure(self): """ Called when a request to the API has failed due to throttling. """ return False def wait(self): """ Returns the recommended next request time in seconds. """ if self.history: remaining_duration = self.duration - (self.now - self.history[-1]) else: remaining_duration = self.duration available_requests = self.num_requests - len(self.history) + 1 if available_requests <= 0: return None return remaining_duration / float(available_requests)
經過繼承SimpleRateThrottle類實現:
class MyThrottle(SimpleRateThrottle): rate = '10/m' # 每分鐘只能訪問10次 def get_cache_key(self, request, view): user = request.user if user: return user xff = request.META.get('HTTP_X_FORWARDED_FOR') remote_addr = request.META.get('REMOTE_ADDR') num_proxies = api_settings.NUM_PROXIES if num_proxies is not None: if num_proxies == 0 or xff is None: return remote_addr addrs = xff.split(',') client_addr = addrs[-min(num_proxies, len(addrs))] return client_addr.strip()
在setting.py中配置:
REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES":[ permissions.utils.MyThrottle, ], 'DEFAULT_THROTTLE_RATES':{ 'scope':'10/minute', } }