四、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、限流可以结合认证进行使用

 

posted @   蜗牛·哥  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有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下安装使用
点击右上角即可分享
微信分享提示