上一次我们学习了10秒之内的频率反爬,原理是缓存中保存前来访问的ip地址10秒,一旦该ip地址再次访问我们网站,我们的服务器会先检查缓存中是否保存了你的ip地址,如果查找到,说明是10秒之内来访问我们的网站,我们就拒绝访问,从而达到了频率反爬。

频率请求限制,频率反爬

现在我们用新的方法来进行频率反爬,做到60秒内限制访问10次。

实现的方法就是,基于当前访问的时间往前推1分钟,我们查看1分钟之内有多少条访问记录,如果超过10条我们就拒绝访问。

只要我们这个value检测到超过10条我们就拒绝该ip地址访问

下面我们用代码来实现这个功能

class HelloMiddle(MiddlewareMixin):  # Mixin 是使混合的意思

    def process_request(self, request):
        print(request.META.get('REMOTE_ADDR'))
        ip = request.META.get('REMOTE_ADDR')

        requests = cache.get(ip, [])  # 从缓存中读取以往存储的ip,并且以[]列表的形式赋值给requests
        while requests and time.time() - requests[-1] > 60:  # 判断有记录存在,且当前时间减去最早记录的时间大于60(也就是时间过期了)
            requests.pop()  # 清洗,把过期的记录清空。这个循环的作用是把过期的时间记录清楚

        if len(requests) > 10:  # 如果有10条没有过期的访问记录,说明访问频率太高,需要拒绝访问
            return HttpResponse('请求次数过于频繁,辣鸡小爬虫回家睡觉吧')

        requests.insert(0, time.time())  # 如果没有满足10次访问,就把当前的访问时间戳插入到列表中的开头
        cache.set(ip, requests, timeout=60)  # 将requests保存到缓存中,并且设置过期时间为60

当我们访问第十一次的时候

这样就可以限制访问次数,注意,被提示访问频繁时只要最早的那次访问记录被清洗掉,我们就又可以访问了,而不是等待60秒才能访问。

假如爬虫依旧在我们警告过后还访问的话,我们可以建立黑名单,对于60秒内频率超过30次的,施行黑名单,直接拉黑ip地址,操作如下。

class HelloMiddle(MiddlewareMixin):  # Mixin 是使混合的意思

    def process_request(self, request):
        print(request.META.get('REMOTE_ADDR'))

        ip = request.META.get('REMOTE_ADDR')
        black_list = cache.get('black', [])

        if ip in black_list:
            return HttpResponse('黑名单,凉凉了')

        requests = cache.get(ip, [])  # 从缓存中读取以往存储的ip,并且以[]列表的形式赋值给requests
        while requests and time.time() - requests[-1] > 60:  # 判断有记录存在,且当前时间减去最早记录的时间大于60(也就是时间过期了)
            requests.pop()  # 清洗,把过期的记录清空。这个循环的作用是把过期的时间记录清楚

        requests.insert(0, time.time())  # 如果没有满足10次访问,就把当前的访问时间戳插入到列表中的开头
        cache.set(ip, requests, timeout=60)  # 将requests保存到缓存中,并且设置过期时间为60

        if len(requests) > 30:    # 如果60s内有30条,拉黑名单
            black_list.append(ip)
            cache.set('black', black_list, timeout=60*60*24)
            return HttpResponse('辣鸡小爬虫,拉你进黑名单')

        if len(requests) > 10:  # 如果有10条没有过期的访问记录,说明访问频率太高,需要拒绝访问
            return HttpResponse('请求次数过于频繁,辣鸡小爬虫回家睡觉吧')

实验:自定义中间件 process_exception

应用交互友好化(提高服务器容错率)

Debug模式下,当我们出现服务器在被访问时出错时,会返回错误码,而关闭debug则会返回500错误,这个错误对于程序员来说是一件很丢脸的事情,所以我们可以利用下面的中间件来提高容错。

  • process_exception(self,request,exception):当视图抛出异常时调用,不主动进行返回或返回HttpResponse对象

    • 视图出错就会报出5xx服务器内部错误,这种错误是程序员最丢人的错误,通过这个切面我们可以全局捕获5xx错误,提高容错率。

我们先制造一个错误的视图,这样可以验证中间件发挥作用
新建urls

 url('^clac/', views.clac, name='clac'),

新建views

def clac(request):
    a = 1
    b = 1
    result = (a+b)/0   # 这行会报错
    return HttpResponse(result)

可以看到访问时

然后我们在中间件MiddleWare.py添加

    def process_exception(self, request, exception):
        print(request, exception)  # 打印出请求类型,和报错原因,用来收集报错信息
        return redirect(reverse('App:index'))  # 发生错误我们就重定向到主页

这样我们只要访问clac这个路由,就会直接重定向到index了,从而将服务器内部错误隐藏了起来,提高了网站的逼格。

同时也会将报错原因打印出来

研究Django自带中间件的原理

我们这里研究的是其中一个Csrf对的中间件

我们ctrl+a进入中间件查看

可以看到这个是继承了我们之前使用过的MiddlewareMixin,我们再点入MiddlewareMIxin

class MiddlewareMixin:
    def __init__(self, get_response=None):
        self.get_response = get_response
        super().__init__()  # 调用的supper的init

    def __call__(self, request):
        response = None
        if hasattr(self, 'process_request'):
            response = self.process_request(request)
        if not response:
            response = self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(request, response)
        return response           # 返回response

可以看到第六行这是call是类装饰器

接着我们回到csrf的中间件代码中

当一个文件的代码很多的时候,我们可以从最左侧的structure来查看当前文件结构

V指的是变量    f开头指的是函数fuction      m是方法


我们点开这个类,可以看到有几个方法

accept接收请求

reject拒绝请求

    def _reject(self, request, reason):
        logger.warning(
            'Forbidden (%s): %s', reason, request.path,
            extra={
                'status_code': 403,  # 拒绝访问时返回的代码为403
                'request': request,
            }
        )
        return _get_failure_view()(request, reason=reason)

get_token获取认证方式

set_token设置认证方式

  • 在response返回的时候会设置token
  • 客户端get请求来的时候设置token,post请求来的时候验证token

还有很多以__开头的方法,这种方法称为魔术方法

中间件的切点函数/方法

process_request

    def process_request(self, request):
        csrf_token = self._get_token(request)    #  利用get_token,获取token
        if csrf_token is not None:            # 如果获取到的token不为空
            # Use same token next time.
            request.META['CSRF_COOKIE'] = csrf_token

我们来观察一下第二行的get_token具体是什么内容

get_token

    def _get_token(self, request):
        if settings.CSRF_USE_SESSIONS:     # 先检查是否用的sessions
            try:
                return request.session.get(CSRF_SESSION_KEY)
            except AttributeError:
                raise ImproperlyConfigured(
                    'CSRF_USE_SESSIONS is enabled, but request.session is not '
                    'set. SessionMiddleware must appear before CsrfViewMiddleware '
                    'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
                )
        else:                               # 假如没有sessions,就试着获取cookies
            try:
                cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
            except KeyError:
                return None

            csrf_token = _sanitize_token(cookie_token)   # 序列化
            if csrf_token != cookie_token:
                # Cookie token needed to be replaced;
                # the cookie needs to be reset.
                request.csrf_cookie_needs_reset = True    # 需要重置
            return csrf_token

process_view

    def process_view(self, request, callback, callback_args, callback_kwargs):
        if getattr(request, 'csrf_processing_done', False):  # 如果完成了csrf的认证,就直接返回None
            return None                                         # 返回None相当于这一层的中间件结束
    

        if getattr(callback, 'csrf_exempt', False):        #exempt是豁免的意思,这里获取csrf_exempt这个属性,默认是False,如果这个属性获取到了就return None
            return None                                    #也就是说如果有csrf豁免权的话也就不需要验证csrf了

        # 从上面两个判断可以知道,如果我们有csrf_processing_done或者csrf_exempt两者其一的属性,我们就可以跳过csrf的认证
        
        
        # Assume that anything not defined as 'safe' by RFC7231 needs protection
        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):  # 也就是说如果是'GET','HEAD','OPTIONS','TRACE'这些请求,就不用csrf认证
            if getattr(request, '_dont_enforce_csrf_checks', False):    # 如果有不强制csrf检查,就返回_accept
                # Mechanism to turn off CSRF checks for test suite.
                # It comes after the creation of CSRF cookies, so that
                # everything else continues to work exactly the same
                # (e.g. cookies are sent, etc.), but before any
                # branches that call reject().
                return self._accept(request)

            if request.is_secure():        # 如果是安全的就逐步进行检查
                # Suppose user visits http://example.com/
                # An active network attacker (man-in-the-middle, MITM) sends a
                # POST form that targets https://example.com/detonate-bomb/ and
                # submits it via JavaScript.
                #
                # The attacker will need to provide a CSRF cookie and token, but
                # that's no problem for a MITM and the session-independent
                # secret we're using. So the MITM can circumvent the CSRF
                # protection. This is true for any HTTP connection, but anyone
                # using HTTPS expects better! For this reason, for
                # https://example.com/ we need additional protection that treats
                # http://example.com/ as completely untrusted. Under HTTPS,
                # Barth et al. found that the Referer header is missing for
                # same-domain requests in only about 0.2% of cases or less, so
                # we can use strict Referer checking.
                referer = request.META.get('HTTP_REFERER')    # 这里的refer指的是上一个页面,当你请求服务器的时候,服务器可以获取到你上一个请求的服务器在哪里
                if referer is None:    # 如果refer为空就直接返回reject,也就是直接拒绝访问( REASON_NO_REFERER拒绝原因是没有上一个页面)
                    return self._reject(request, REASON_NO_REFERER)

                referer = urlparse(referer)    # 如果有refer就转换一下

                # Make sure we have a valid URL for Referer.
                if '' in (referer.scheme, referer.netloc):    # REASON_MALFORMED_REFERER拒绝原因上一页格式有问题
                    return self._reject(request, REASON_MALFORMED_REFERER)

                # Ensure that our Referer is also secure.
                if referer.scheme != 'https':    # 如果不是https协议,也拒绝访问,因为csrf是安全的请求
                    return self._reject(request, REASON_INSECURE_REFERER)

                # If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
                # match on host:port. If not, obey the cookie rules (or those
                # for the session cookie, if CSRF_USE_SESSIONS).
                good_referer = (        # 这里新建一个good_referer
                    settings.SESSION_COOKIE_DOMAIN
                    if settings.CSRF_USE_SESSIONS    # 从sessions里获取
                    else settings.CSRF_COOKIE_DOMAIN # 否则从cookie中获取
                )
                if good_referer is not None:    # 如果刚刚创建的不是空的
                    server_port = request.get_port()    # 获取端口
                    if server_port not in ('443', '80'): # 如果获取到的端口不是443或者80这种默认端口,那么就用自己上一步获取到的端口
                        good_referer = '%s:%s' % (good_referer, server_port)
              
                else:    # 如果good_referer是空的,就直接获取host赋值给good_referer
                    # request.get_host() includes the port.
                    good_referer = request.get_host()

                # Here we generate a list of all acceptable HTTP referers,
                # including the current host since that has been validated
                # upstream.
                good_hosts = list(settings.CSRF_TRUSTED_ORIGINS) # 转换成列表
                good_hosts.append(good_referer) # 追加了我们刚刚创建的referer进入good_hosts

                if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
                    # 意思是如果 any()为False,就拒绝访问,也就是如果good_gosts中的元素都为假False,就拒绝访问
                    reason = REASON_BAD_REFERER % referer.geturl()
                    return self._reject(request, reason)

            csrf_token = request.META.get('CSRF_COOKIE')    # 令csrf_token获取CSRF_COOKIE
            if csrf_token is None:    # 如果获取到的为空,就拒绝访问
                # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
                # and in this way we can avoid all CSRF attacks, including login
                # CSRF.
                return self._reject(request, REASON_NO_CSRF_COOKIE)

            # Check non-cookie token for match.
            request_csrf_token = ""        # 如果csrf_token不为空的
            if request.method == "POST":    # 如果请求是post
                try:    # 从form表单中获取到csrfmiddlewaretoken,如果没有获取到就赋值空字符串
                    request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
                except IOError:
                    # Handle a broken connection before we've completed reading
                    # the POST data. process_view shouldn't raise any
                    # exceptions, so we'll ignore and serve the user a 403
                    # (assuming they're still listening, which they probably
                    # aren't because of the error).
                    pass

            if request_csrf_token == "":    # 判断如果为空
                # Fall back to X-CSRFToken, to make things easier for AJAX,
                # and possible for PUT/DELETE.
                request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')  # 从settings中去获取并赋值给request_csrf_token

            request_csrf_token = _sanitize_token(request_csrf_token) # 转换一下tonken
            if not _compare_salted_tokens(request_csrf_token, csrf_token):   # 检验的token是否加盐,如果没有匹配上token,就拒绝访问
                return self._reject(request, REASON_BAD_TOKEN)                 # 也就是这里验证的csrf

        return self._accept(request)    # 如果通过验证的token就返回接受,可以正常访问

csrf实际上实在process_view中验证的

解释:

  • 第76行使用了any()函数:

  • 第76行的 is_same_domain(referer.netloc, host) for host in good_hosts 使用的是列表推导式:

如何让特定请求跳过csrf验证

假如我们的项目开发完了,有一个函数,我们只想给某一个请求,去除scrf验证,该怎么实现?

首先建立一个模板login.html,用来充当我们登陆的界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form action="{% url 'app:login' %}" method="post">
    <span>用户名</span><input type="text" name="username" placeholder="嘤嘤嘤">
    <br>
    <button>嘤嘤嘤嘤</button>
</form>
</body>
</html>

再建立一个url

url('^login/', views.login, name='login')

views:

def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == 'POST':
        return HttpResponse('post请求成功')

然后我们来访问网页,随便输入点东西,然后点击嘤嘤嘤嘤


解决csrf禁止访问的办法

我们出现了这个csrf禁止访问,在之前我们通过:

  • 添加csrf标签,
  • 注释掉csrf验证

来解决csrf验证失败,现在我们学习第三种方法来解决这个问题

  • 第三种方法:添加豁免属性

我们在views中添加一个豁免属性的装饰器

@csrf_exempt
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    elif request.method == 'POST':
        return HttpResponse('post请求成功')

添加装饰器之后就可以免除csrf验证
再次访问:成功访问

这个方法的原理就是在process_view中

有两种可以跳过csrf验证的判断,第一种我们没见过,它的方法不是装饰器在这里就不能使用了,但是第二种是csrf豁免,只要有这个属性我们就可以豁免了。

第一种其实也是验证csrf_processing_done这个属性,我们可以自己写一个中间件加一个属性,等于True,我们也可以实现。

最后修改:2024 年 03 月 13 日
如果觉得我的文章对你有用,请随意赞赏