06 DRF-限流

1 限流介绍

开发过程中,某个接口不想让用户访问过于频繁,限流机制,例如:短信接口(ip限制,验证码,防止爬虫机制)

限制访问频率前提:

  • 已登录用户,用户的主键,ID,用户名
  • 未登录,IP唯一表示

如何限制:

  • 用户id:【访问时间1, 访问时间2, 】 (比如:10分钟3次)
    • 获取当前时间
    • 当前时间-10分钟=计数开始时间
    • 删除 新时间 后的老时间
    • 计算列表长度>3 不能访问; < 3 可以访问

2 快速使用

  • 第一步:创建限流文件throttle.py,编写类
    依赖:

    pip install django-redis
    

    settings.py:

    CACHES = {
    	"default": {
    		"BACKEND": "django_redis.cache.RedisCache",
    		"LOCATION": "redis://127.0.0.1:6379",
    		"OPTIONS": {
    			"CLIENT_CLASS": "django_redis.client.DefaultClient",
    			"PASSWORD": "xxx",
    		}
    	}
    }
    
    from rest_framework.throttling import BaseThrottle, SimpleRateThrottle
    from django.core.cache import cache as default_cache
    
    
    class MyThrottle(SimpleRateThrottle):
    	scope = "XXX"
    
    	cache = default_cache  # 访问记录存放在django的缓存中(需设置缓存)
    
    	# 设置访问频率,例如:1分钟允许访问10次
    	# 其他:'s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day'
    	THROTTLE_RATES = {"XXX": "5/m"}
    
    	# 要生成的唯一标识
    	def get_cache_key(self, request, view):
    		if request.user:
    			ident = request.user.pk  # 用户id
    		else:
    			ident = self.get_ident(request)  # 获取请求用户IP(去request中找请求头)
    
    		# 没有登录 ident=ip地址
    		# 登陆 ident=user.pk
    		return self.cache_format % {'scope': self.scope, 'ident': ident}
    
  • 第二步:任意视图类中应用

    from ext.throttle import MyThrottle
    
    class LoginView(APIView):
    	throttle_classes = [MyThrottle, ]
    

    可以局部也可以全局,一般是局部使用


3 源码分析

点击查看代码
class BaseThrottle:
    def allow_request(self, request, view):
        raise NotImplementedError('.allow_request() must be overridden')

    def get_ident(self, request):
        # 获取ip地址
        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):
        return None


class SimpleRateThrottle(BaseThrottle):

    cache = default_cache
    timer = time.time
    cache_format = 'throttle_%(scope)s_%(ident)s'
    scope = None
    THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES

    def __init__(self):
        if not getattr(self, 'rate', None):
            self.rate = self.get_rate() # '5/m'  、 '3/m'
        # 解析字符串
        self.num_requests, self.duration = self.parse_rate(self.rate)

    def get_cache_key(self, request, view):
        raise NotImplementedError('.get_cache_key() must be overridden')

    def get_rate(self):
        if not getattr(self, 'scope', None):
            msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
                   self.__class__.__name__)
            raise ImproperlyConfigured(msg)

        try:
            # THROTTLE_RATES = {"XXX": "5/m"} XXX
            return self.THROTTLE_RATES[self.scope]
        except KeyError:
            msg = "No default throttle rate set for '%s' scope" % self.scope
            raise ImproperlyConfigured(msg)

    def parse_rate(self, rate):
        # '5/m'  、 '3/m'
        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):
        if self.rate is None:
            return True

        # 获取用户的唯一标识
        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        # 获取历史访问记录
        self.history = self.cache.get(self.key, [])
        # 获取当前时间
        self.now = self.timer()

        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()
        if len(self.history) >= self.num_requests:
            return self.throttle_failure()  # 超过了限制
        return self.throttle_success()  # 正常访问

    def throttle_success(self):
        self.history.insert(0, self.now)
        self.cache.set(self.key, self.history, self.duration)
        return True

    def throttle_failure(self):
        return False

    def wait(self):
        # 计算还需要等待多久
        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)



from rest_framework.throttling import BaseThrottle, SimpleRateThrottle


class MyThrottle(SimpleRateThrottle):
    scope = "XXX"
    cache = default_cache  # 访问记录存放在django的缓存中(需设置缓存)
    THROTTLE_RATES = {"XXX": "5/m"}

    def get_cache_key(self, request, view):
        if request.user:
            ident = request.user.pk  # 用户id
        else:
            ident = self.get_ident(request)  # 获取请求用户IP(去request中找请求头)

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



class LoginView(APIView):
    throttle_classes = [MyThrottle, ]


---------------------------------------------------------------------------------


class Request:
    def __init__(self, request, authenticators=None):
        self.authenticators = authenticators or ()

    @property
    def user(self):
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user

    @user.setter
    def user(self, value):
        self._user = value
        self._request.user = value

    def _authenticate(self):
        # 读取每个认证组件的对象,执行 authenticate 方法,self=request对象
        for authenticator in self.authenticators:
            try:
                user_auth_tuple = authenticator.authenticate(self)
            except exceptions.APIException:
                self._not_authenticated()
                raise

            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple
                return

        self._not_authenticated()

    def _not_authenticated(self):  # 匿名访问
        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


class APIView(View):
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES

    def get_throttles(self):
        # 限流类的对象列表
        return [throttle() for throttle in self.throttle_classes]

    def check_throttles(self, request):
        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)

    def throttled(self, request, wait):
        raise exceptions.Throttled(wait)  # 抛出异常

    def initialize_request(self, request, *args, **kwargs):
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            authenticators=self.get_authenticators(),  # [对象1, 对象2,...]
            ...
        )


    def dispatch(self, request, *args, **kwargs):
    self.args = args  # url传递的参数接收
    self.kwargs = kwargs  # url传递的参数接收

    # 第一步:请求的封装(django的request对象 + authenticators认证组件)—> 加载认证组件的过车用
    request = self.initialize_request(request, *args, **kwargs)
    self.request = request
    self.headers = self.default_response_headers  # deprecate?

    try:
        # 第二步:request
        self.initial(request, *args, **kwargs)

        # Get the appropriate handler method
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(),
                              self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed

        # 第三步:执行视图函数
        response = handler(request, *args, **kwargs)

    except Exception as exc:
        response = self.handle_exception(exc)

    self.response = self.finalize_response(request, response, *args, **kwargs)
    return self.response



class LoginView(APIView):
    throttle_classes = [MyThrottle, ]


通过源码分析,我们可以得知:SimpleRateThrottle类中有一个全局变量,在全局配置后,自己的限流类中可以不用配置了。

REST_FRAMEWORK = {
    "UNAUTHENTICATED_USER": None,
    'DEFAULT_THROTTLE_RATES': {'XXX': '5/m', 'X2': '3/m'},  # 全局限流规则 
}
from rest_framework.throttling import BaseThrottle, SimpleRateThrottle
from django.core.cache import cache as default_cache


class MyThrottle(SimpleRateThrottle):
    scope = "XXX"
    cache = default_cache  # 访问记录存放在django的缓存中(需设置缓存)

    def get_cache_key(self, request, view):
        if request.user:
            ident = request.user.pk  # 用户id
        else:
            ident = self.get_ident(request)  # 获取请求用户IP(去request中找请求头)

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

4 小小应用

条件:匿名用户10/m,登录用户5/m

from django.db import models


class UserInfo(models.Model):
    role = models.IntegerField(verbose_name='角色',
                               choices=((1, '总监'), (2, '经理'), (3, '员工')),
                               default=3)

    username = models.CharField(verbose_name='用户名', max_length=32)
    password = models.CharField(verbose_name='密码', max_length=64)
    # 临时使用
    token = models.CharField(verbose_name='TOKEN', max_length=64,
                             null=True, blank=True)

image

那么如何自定义,限流抛出异常的返回值呢?

posted @ 2022-10-18 16:27  角角边  Views(36)  Comments(0)    收藏  举报