Django CsrfViewMiddleware 源码学习

这个中间件主要是防止 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 的状态,我对状态还有一些不清楚,望大佬赐教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Web框架之Django: (1)简介: Django是一个由Python写成开源的重量级Web应用框架,采用MT...
    老肖阅读 3,045评论 0 18
  • 本文是 Django 官网文档的翻译。官网链接:https://docs.djangoproject.com/en...
    学以致用123阅读 5,651评论 0 5
  • 模块间联系越多,其耦合性越强,同时表明其独立性越差( 降低耦合性,可以提高其独立性)。软件设计中通常用耦合度和内聚...
    riverstation阅读 2,058评论 0 8
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 《守望成长》 作者:陈序 从只有手臂大小的孩子,到现在这个身体健康的青年,我在这世界上度过了15年的时光,...
    陈序原创阅读 506评论 0 0