Django CsrfViewMiddleware 源码学习

2018-08-20  本文已影响0人  vckah

这个中间件主要是防止 CSRF 攻击的。
首先来总体概括一下:

csrf_token 是一个全局变量,是 django 自动生成的,csrf_token 不需要 django 存储。每次刷新生成一个新的 token,然后将 token 返回给前端,同时伴随着 set-cookie 操作,把 token 写进浏览器 cookie。验证请求的时候,只需拿 cookie 里面的 csrftoken 和 csrf_token 和提交表单里面的 csrfmiddlewaretoken 值进行比较即可。如果一样,即请求有效。这里的比较不是看两个值是否相等,而是通过一系列解密判断的。

接着来看一看源码:

class CsrfViewMiddleware(MiddlewareMixin):
    def _accept(self, request):
        pass
    def _reject(self, request, reason):
        pass
    def _get_token(self, request):
        pass
    def _set_token(self, request, response):
        pass
    def process_view(self, request, callback, callback_args, callback_kwargs):
        pass
    def process_response(self, request, response):
        pass

主要是 process_viewprocess_response,同样,view 主要是在 url 匹配之后,确定视图函数之前,而 response 是在执行完视图函数之后。接下来来看一下具体流程:

def process_view(self, request, callback, callback_args, callback_kwargs):
    if getattr(request, 'csrf_processing_done', False):
        return None

    csrf_token = self._get_token(request)
    if csrf_token is not None:
        # Use same token next time.
        request.META['CSRF_COOKIE'] = csrf_token

    if getattr(callback, 'csrf_exempt', False):
        return None

    if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
        if getattr(request, '_dont_enforce_csrf_checks', False):
            return self._accept(request)

        if request.is_secure():
            referer = force_text(
                request.META.get('HTTP_REFERER'),
                strings_only=True,
                errors='replace'
            )
            if referer is None:
                return self._reject(request, REASON_NO_REFERER)

            referer = urlparse(referer)

            if '' in (referer.scheme, referer.netloc):
                return self._reject(request, REASON_MALFORMED_REFERER)

            if referer.scheme != 'https':
                return self._reject(request, REASON_INSECURE_REFERER)

            good_referer = (
                settings.SESSION_COOKIE_DOMAIN
                if settings.CSRF_USE_SESSIONS
                else settings.CSRF_COOKIE_DOMAIN
            )
            if good_referer is not None:
                server_port = request.get_port()
                if server_port not in ('443', '80'):
                    good_referer = '%s:%s' % (good_referer, server_port)
            else:
                # request.get_host() includes the port.
                good_referer = request.get_host()

            good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
            good_hosts.append(good_referer)

            if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
                reason = REASON_BAD_REFERER % referer.geturl()
                return self._reject(request, reason)

        if csrf_token is None:
            return self._reject(request, REASON_NO_CSRF_COOKIE)

        request_csrf_token = ""
        if request.method == "POST":
            try:
                request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
            except IOError:
                pass

        if request_csrf_token == "":
            request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

        request_csrf_token = _sanitize_token(request_csrf_token)
        if not _compare_salted_tokens(request_csrf_token, csrf_token):
            return self._reject(request, REASON_BAD_TOKEN)

    return self._accept(request)

这里我们思考一下为什么实现了 process_view 呢?process_view 是在 匹配 url 后准备执行视图函数前才调用的。因为某些 url 对应的视图函数里不准备使用 csrf 验证,那么验证这步干什么呢?
首先判断 request 中有没有 csrf_processing_done ,这个是干什么的呢。然后判断回调函数时候是否有 csrf_exempt,因为这个属性代表不需要 csrf token 验证。
然后 _get_token 里面判断有没有 token,有的话比对一下,否则返回 None。接着判断视图函数是否需要 csrf 防护,如果没有的话,当然也不用分析了。
接着判断 HTTP 方法,如果不是 ('GET', 'HEAD', 'OPTIONS', 'TRACE') 这几种,那么继续执行。因为 rfc 定义这几种方法都是安全的。然后关闭测试套件的 csrf 检查,这里不明白。

下来判断是否是 https 请求,这里进行了一些检查,说实话,这里也没看懂。

接下来就是去 request.META 去获取 CSRF_COOKIE,如果没有则返回 403 页面。接着如果是 POST 请求就去 request.POST 中获取 csrfmiddlewaretoken 值,这里有可能会报错吗?如果没有,则会可能是其它请求或者是 AJAX 请求,则去 request.META 中去取,然后这里进行了 _sanitize_token 函数,主要是检查 request_csrf_token 是否符合规范
详情如下

def _sanitize_token(token):
    # Allow only ASCII alphanumerics
    if re.search('[^a-zA-Z0-9]', token):
        return _get_new_csrf_token()
    elif len(token) == CSRF_TOKEN_LENGTH:
        return token
    elif len(token) == CSRF_SECRET_LENGTH:
        return _salt_cipher_secret(token)
    return _get_new_csrf_token()

如果不合规,直接构造一个新的 csrf_token,如果合规,返回原 csrf_token
最后比较两个值,一个是表单里面的,一个是请求头中的 cookiecsrf_token。这里检验的话,有一个好办法,在浏览器中查看请求头和提交的参数。这里里面使用和 hmac 来验证。具体的可以跟踪看看。返回真就进入 _accept 函数,里面奇怪定义了equest.csrf_processing_done = True,不解,望大佬赐教。如果不相等,则返回 403 页面。

下来看一看 process_response:

def process_response(self, request, response):
    self._set_token(request, response)
    response.csrf_cookie_set = True
    return response

这里是核心部分,至于检查那些参数,我也不是很懂。这里 _set_token,然后进去 response.set_cookie 操作,设置 cookie 值为 csrftoken
好了,就到这里吧,里面还有一些细节没有掌握,例如 Django 随时在判断 request 和 response 的状态,我对状态还有一些不清楚,望大佬赐教。

上一篇下一篇

猜你喜欢

热点阅读