四、REST framework的三大组件之一:限流
1、介绍
1.1、开发过程中,某个接口不想用户访问过于频繁,使用限流机制进行控制,比如:现在1分钟发5次等。
1.2、限流时,需要基于某个条件生成唯一标识进行限制,比如获取访问对象的IP、用户信息的主键、ID、用户名等等进行限制,
2、限制示例,1分钟访问5次"
步骤一:唯一标识": [12:00:30,12:01:05,12:00:50,12:00:20,12:00:05],列表为访问记录
步骤二:获取当前时间 12:00:15
步骤三:当前时间减去1分钟=计数时间, 12:01:15 - 60s<1分钟> = 12:00:45
步骤四:通过计数时间在唯一标识的列表中,找到少于 12:00:45的时间,并进行删除,得到列表 [12:00:30,12:01:05,12:00:50]
步骤五:计数等到列表的长度,如果列表长度 > 5<次>,错误限制访问,反之,正常访问。
3、在ext中新建myThrottle.py,并添加限流类
# 导入rest_framework的限流类 from rest_framework.throttling import BaseThrottle class MyThrottle(BaseThrottle): # 继承BaseThrottle,需要重写allow_request方法 def allow_request(self, request, view): """ Return `True` if the request should be allowed, `False` otherwise. """ # 返回True 表示允许访问 return True
4、限流类BaseThrottle的源码解释
class BaseThrottle: """ Rate throttling of requests. """ # 限流入口方法 def allow_request(self, request, view): """ Return `True` if the request should be allowed, `False` otherwise. """ raise NotImplementedError('.allow_request() must be overridden') # 获取IP地址,用于后续构建唯一标识 def get_ident(self, request): """ Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR if present and number of proxies is > 0. If not use all of HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR. """ 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() return ''.join(xff.split()) if xff else remote_addr # 等待时间 def wait(self): """ Optionally, return a recommended number of seconds to wait before the next request. """ return None
5、通过限流类BaseThrottle,可以看出我们要使用基类的话必须重写它的三个方法,但是DRF为了避免我们进行复杂操作,提供了SimpleRateThrottle类,修改MyThrottle类
# 导入rest_framework的限流类 from rest_framework.throttling import SimpleRateThrottle class MyThrottle(SimpleRateThrottle): # 使用SimpleRateThrottle需要重写get_cache_key方法,get_cache_key生成唯一标识 def get_cache_key(self, request, view): """ 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')
6、在认证时,我们对request.user进行了赋值,及表示数据库表的实例,因此,这里我们可以获取用户的唯一ID,修改方法get_cache_key
# 导入rest_framework的限流类 from rest_framework.throttling import SimpleRateThrottle class MyThrottle(SimpleRateThrottle): # 定义scope scope = "xxx" # 使用SimpleRateThrottle需要重写get_cache_key方法,get_cache_key生成唯一标识 # 目的:防止唯一标识重复 def get_cache_key(self, request, view): # 判断是否有request.user if request.user: # 如果有request.user,获取唯一标识,及主键 ident = request.user.pk else: # 如果没有,调用父类的get_ident,获取请求用户的IP<在request的请求头中查找> ident = self.get_ident(request) # 对self.cache_format进行,字符串格式化 cache_format = 'throttle_%(scope)s_%(ident)s' # 调用父类的self.scope,父类中的scope = None,因此可以自己进行定义 return self.cache_format % { 'scope': self.scope, 'ident': ident }
7、在 SimpleRateThrottle的__init__方法,通过初始化获取了 self.rate = self.get_rate(),get_rate()源码介绍
def get_rate(self): """ Determine the string representation of the allowed request rate. """ # 反射判断是否有scope,没有抛出异常 if not getattr(self, 'scope', None): msg = ("You must set either `.scope` or `.rate` for '%s' throttle" % self.__class__.__name__) raise ImproperlyConfigured(msg) # 验证是否有THROTTLE_RATES属性,没有抛出异常 try: # 没有定义THROTTLE_RATES时,通过配置,获取全局设置的THROTTLE_RATES,当前没有进行全局设置,不手动设置会抛出异常 # 获取配置信息,如几秒访问几次 {"xxx": "5/s"} # self.THROTTLE_RATES[self.scope]是通过self.scope为键,获取值,因此配置时THROTTLE_RATES的键必须和scope一致 return self.THROTTLE_RATES[self.scope] except KeyError: msg = "No default throttle rate set for '%s' scope" % self.scope raise ImproperlyConfigured(msg)
8、通过get_rate()源码我们知道还需要定义 THROTTLE_RATES属性,修改get_cache_key,添加THROTTLE_RATES配置
class MyThrottle(SimpleRateThrottle): # 定义scope scope = "xxx" # 配置THROTTLE_RATES,键与scope保持一致 THROTTLE_RATES = {"xxx": "10/m"} # 使用SimpleRateThrottle需要重写get_cache_key方法,get_cache_key生成唯一标识 # 目的:防止唯一标识重复 def get_cache_key(self, request, view): # 判断是否有request.user if request.user: # 如果有request.user,获取唯一标识,及主键 ident = request.user.pk else: # 如果没有,调用父类的get_ident,获取请求用户的IP<在request的请求头中查找> ident = self.get_ident(request) # 对self.cache_format进行,字符串格式化 cache_format = 'throttle_%(scope)s_%(ident)s' # 调用父类的self.scope,父类中的scope = None,因此可以自己进行定义 return self.cache_format % { 'scope': self.scope, 'ident': ident }
9、在SimpleRateThrottle源码中,cache = default_cache为调用Redis,并把访问记录放在Redis中,因此需要设置Redis连接,配置setting
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://IP:端口/数据库编号", # Redis服务器地址和端口,以及使用的数据库编号(默认为0,这里改为1) "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100}, "DECODE_RESPONSES": True, # 自动将Redis存储的字节串解码为字符串 } } }
10,配置视图函数,并使用postman进行接口访问
class LoginView(MyAPIView): # authentication_classes 为认证类的默认配置参数,必须怎么写,详细过程可以查看源码 # 当不使用认证类时,列表为空 # 请注意,当类中使用authentication_classes=[认证类1, 认证类2, ...]时,认证默认使用就近原则,使用类中的authentication_classes authentication_classes = [QueryParamsAuthentication] # 通过url认证 # 请注意,当类中使用permission_classes=[权限类1, 权限类2, ...]时,权限默认使用就近原则,使用类中的permission_classes permission_classes = [MyPermission1, MyPermission2] # role=1和2的的权限 # 应用限流类,throttle_classes限流类的默认配置参数,必须怎么写,详细过程可以查看源码 throttle_classes = [MyThrottle] def get(self, request, *args, **kwargs): return Response('GET') # 接口返回: 前5次,正常返回 # 当访问超过五次时,返回 # { # "detail": "请求超过了限速。 Expected available in 56 seconds." # }
11、结合源码可以看出,限流类也支持全局配置使用,使用方法请自行查看源码,但是这种功能我们基本不在全局使用。
12、在第8个步骤中,如果不定义THROTTLE_RATES, 它会从配置文件中查找,因此我们可以在全局配置THROTTLE_RATES属性,配置完成后,删除MyThrottle中的THROTTLE_RATES,接口访问正常
REST_FRAMEWORK = { "UNAUTHENTICATED_USER": None, # 添加DEFAULT_PERMISSION_CLASSES使其支持全局认证 "DEFAULT_AUTHENTICATION_CLASSES": ["auto_project.ext.myAuthentication.QueryParamsAuthentication", "auto_project.ext.myAuthentication.HeaderAuthentication", "auto_project.ext.myAuthentication.BodyAuthentication", "auto_project.ext.myAuthentication.NoAuthentication", ], # 添加DEFAULT_PERMISSION_CLASSES,所有视图默认全部应用权限 "DEFAULT_PERMISSION_CLASSES": ["auto_project.ext.MyPermission.MyPermission1"], # 添加限流配置 "DEFAULT_THROTTLE_RATES": {"xxx": "5/m", "yyy": "10/m"}, # 版本控制 "VERSION_PARAM": "version", "DEFAULT_VERSION": "v1", "ALLOWED_VERSIONS": ["v1", "v2"], "NON_FIELD_ERRORS_KEY": "全局验证" }
13、源码执行流程,请注意类对象实例化时,先触发__init__方法
13.1、限流调用流程: as_view -> dispatch,在dispatch中通过 self.initial(request, *args, **kwargs),调用限流方法check_throttles
def check_throttles(self, request): """ Check if request should be throttled. Raises an appropriate exception if the request is throttled. """ # 创建空列表 throttle_durations = [] # 循环throttle_classes类对象 for throttle in self.get_throttles(): # 获取对象后,调用allow_request,not True = False, 当为True时,才执行下面的流程 # allow_request,函数入口方法 if not throttle.allow_request(request, self): # 调用throttle.wait(),相当于还需要等待多长时间 # wait执行流程:当前时间减去最早的访问记录,然后使用间隔时间减去得到的值,得到还需要等待多久 throttle_durations.append(throttle.wait()) # 如果超出限流现在,调用if流程 if throttle_durations: # Filter out `None` values which may happen in case of config / rate # changes, see #1438 # 删除None durations = [ duration for duration in throttle_durations if duration is not None ] # 获取最大限流,如,使用了很多限流,取其中最大的限流 duration = max(durations, default=None) # 找到视图中的throttled方法,抛出异常 self.throttled(request, duration)
14、定制返回code和描述信息
14.1、根据源码到抛出异常时,调用的是self.throttled(request, duration)方法,该方法在APIview中定义
def throttled(self, request, wait): """ If request is throttled, determine what kind of exception to raise. """ raise exceptions.Throttled(wait)
14.2、self.throttled(request, duration)方法调用exceptions.Throttled(wait)
class Throttled(APIException): status_code = status.HTTP_429_TOO_MANY_REQUESTS default_detail = _('Request was throttled.') extra_detail_singular = _('Expected available in {wait} second.') extra_detail_plural = _('Expected available in {wait} seconds.') default_code = 'throttled' def __init__(self, wait=None, detail=None, code=None): if detail is None: detail = force_str(self.default_detail) if wait is not None: wait = math.ceil(wait) detail = ' '.join(( detail, force_str(ngettext(self.extra_detail_singular.format(wait=wait), self.extra_detail_plural.format(wait=wait), wait)))) self.wait = wait super().__init__(detail, code)
14.3、exceptions.Throttled只是对detail, code进行了设置,那么我们可以重写throttled方法,而后自定义我们自己的Throttled<在之前我们定义的MyAPIView中重写>
# 重写 APIView的throttled方法 def throttled(self, request, wait): # 返回自定义异常返回值 raise Throttled(wait) # 导入相PIException异常类和math标准数学库math from rest_framework.exceptions import APIException import math # 编写自己的Throttled class Throttled(APIException): # __init__方法 def __init__(self, wait=None): if wait is not None: # wait不为空时,去整数 wait = math.ceil(wait) # 赋值 self.wait = wait # 调用父类的__init__方法,只对当前类做处理,后续逻辑还是通过APIException处理 super().__init__({'code': 100005, 'msg': f'请求超过次数,请等待{wait}s后,进行访问'})
14,4、再次访问接口时,超出限制后,接口返回
总结:
1、限流不要在全局应用
2、重写限流时的返回结果,有助于定制后续的状态码
3、限流可以结合认证进行使用
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2017-12-26 2017-12-26--mysql(5.6.15),linux下安装使用