比如我们有一个用户大转盘抽奖的功能,需要规定用户在一个小时内只能抽奖3次,那此时对接口的访问频率限制就显得尤为重要
其实在restframework中已经为我们提供了频率限制的组件
先捋一下请求到APIview的过程:
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到底做了什么呢?
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很相似,分为以下几个步骤:
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表示正确允许访问,就执行下次循环,检查下一个频率控制对象
按照之前的套路,throttle组件中应该有个基础的throttle类,找一下:
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.throttled
# 抛出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)
实现:
一般来说,接口如果不做登录限制,那就会允许匿名用户和已登录用户都能访问。所以这个接口就要考虑能对匿名用户和登录用户都进行访问频率限制:
思路:
已经登录用户可以根据身份做判断,固定时间内,同一个用户的身份只能访问限定次数
未登录用户可通过IP地址判断,对同一个IP的请求进行限制
1 class TestThrottle(BaseThrottle): 2 ctime = time.time 3 4 def get_ident(self, request): 5 """ 6 根据用户IP和代理IP,当做请求者的唯一IP 7 Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR 8 if present and number of proxies is > 0. If not use all of 9 HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR. 10 """ 11 xff = request.META.get('HTTP_X_FORWARDED_FOR') 12 remote_addr = request.META.get('REMOTE_ADDR') 13 num_proxies = api_settings.NUM_PROXIES 14 15 if num_proxies is not None: 16 if num_proxies == 0 or xff is None: 17 return remote_addr 18 addrs = xff.split(',') 19 client_addr = addrs[-min(num_proxies, len(addrs))] 20 return client_addr.strip() 21 22 return ''.join(xff.split()) if xff else remote_addr 23 24 def allow_request(self, request, view): 25 """ 26 是否仍然在允许范围内 27 Return `True` if the request should be allowed, `False` otherwise. 28 :param request: 29 :param view: 30 :return: True,表示可以通过;False表示已超过限制,不允许访问 31 """ 32 # 获取用户唯一标识(如:IP) 33 34 # 允许一分钟访问10次 35 num_request = 10 36 time_request = 60 37 38 now = self.ctime() 39 ident = self.get_ident(request) 40 self.ident = ident 41 if ident not in RECORD: 42 RECORD[ident] = [now, ] 43 return True 44 history = RECORD[ident] 45 while history and history[-1] <= now - time_request: 46 history.pop() 47 if len(history) < num_request: 48 history.insert(0, now) 49 return True 50 51 def wait(self): 52 """ 53 多少秒后可以允许继续访问 54 Optionally, return a recommended number of seconds to wait before 55 the next request. 56 """ 57 last_time = RECORD[self.ident][0] 58 now = self.ctime() 59 return int(60 + last_time - now)
1 class MemberPrograms(APIView): 2 throttle_classes = [TestThrottle, ] 3 4 def get(self, request): 5 programs = MemberProgram.objects.all().values() 6 return JsonResponse(list(programs), safe=False)
测试: