Django Documentation

csrf保护基于以下: 
1. 一个CSRF cookie 基于一个随机生成的值,其他网站无法得到。此cookie由CsrfViewMiddleware产生。它与每个调用django.middleware.csrf.get_token()(这是一个用于取回CSRF token的方法)的响应一起发送,如果它尚未在请求上设置的话。
为了防止BREACH攻击,token不仅仅是秘密;随机的salt被置于secret之前并用来加密它。出于安全原因,每次用户登录时都会更改密钥的值。

  1. 所有传出POST表单中都有一个名为csrfmiddlewaretoken的隐藏表单字段。此字段的值同样是秘密的值。salt添加到它并用于加扰它。每次调用get_token()时都会重新生成salt,以便在每个此类响应中更改表单字段值。这部分由template的{% csrf_token %}完成。

  2. 对于未使用HTTP GETHEADOPTIONSTRACE的所有传入请求,必须带有CSRF cookie,并且csrfmiddlewaretoken字段必须存在且正确。如果不是,用户将收到403错误。
    验证csrfmiddlewaretoken字段值时,只将secret而不是整个token与cookie值中的secret进行比较。这允许使用不断变化的token。虽然每个请求都可以使用自己的token,但secret仍然是所有人共同的。
    此检查由CsrfViewMiddleware完成。

  3. 此外,对于HTTPS请求,严格的引用检查由CsrfViewMiddleware完成。这意味着即使子域可以在您的域上设置或修改cookie,它也不能强制用户发布到您的应用程序,因为该请求不会来自您自己的确切域。 这也解决了在使用会话独立秘密时在HTTPS下可能发生的中间人攻击,因为即使在HTTPS下与站点通信时,HTTP Set-Cookie标头(不幸)也被客户接受了。 。 (对HTTP请求不进行引用检查,因为在HTTP下,Referer头的存在不够可靠。) 如果设置了CSRF_COOKIE_DOMAIN设置,则会将引用者与其进行比较。此设置支持子域。例如,CSRF_COOKIE_DOMAIN ='.example.com'将允许来自www.example.comapi.example.com的POST请求。如果未设置该设置,则referer必须与HTTP Host标头匹配。 可以使用CSRF_TRUSTED_ORIGINS设置将已接受的引用扩展到当前主机或cookie域之外。

流程图

CsrfViewMiddleware.process_request

# django/middleware/csrf.py
class CsrfViewMiddleware(MiddlewareMixin):
def process_request(self, request):
csrf_token = self._get_token(request)
# 第一次访问,csrf_token返回None, if csrf_token is not None:
# Use same token next time.
request.META['CSRF_COOKIE'] = csrf_token
# request.META 是一个 Python 字典,包含了所有本次 HTTP 请求的 Header
# 信息,比如用户 IP 地址和用户Agent(通常是浏览器的名称和版本号)。

  settings = LazySettings()

方法_get_token,从名字上来看就是获取token,_get_token在后面多处地方都有用到

# django/middleware/csrf.py
def _get_token(self, request):
# CSRF_USE_SESSIONS在django/conf/global_settings.py,默认为False,执行else
if settings.CSRF_USE_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:
try:
cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
# CSRF_SESSION_KEY= "csrftoken"
except KeyError:
# 第一次访问的时候 request.COOKIES = {},所以直接返回
return None csrf_token = _sanitize_token(cookie_token)
# csrf 对不上 cookie里 的 token,标记csrf_cookie_needs_reset=True,
# 在process_response的方法中判定
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
# /django/middleware/csrf.py

CSRF_SECRET_LENGTH = 32
CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH def _sanitize_token(token):
# Allow only ASCII alphanumerics
# 仅允许ASCII字母数字
if re.search('[^a-zA-Z0-9]', token):
return _get_new_csrf_token()

先跳转到_get_new_csrf_token(),看他的生成方法

def _get_new_csrf_token():
return _salt_cipher_secret(_get_new_csrf_string()) CSRF_SECRET_LENGTH = 32
CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH def _get_new_csrf_string():
return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS) def _salt_cipher_secret(secret):
"""
Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a
token by adding a salt and using it to encrypt the secret. 给定一个secret(假设是一串CSRF_ALLOWED_CHARS),通过添加一个随机生成值并使用它来加
密secret来生成一个token。 """
salt = _get_new_csrf_string()
chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
return salt + cipher
# django/utils/crypto.py
def get_random_string(length=12,
allowed_chars='abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
"""
Return a securely generated random string.
返回安全生成的随机字符串。 The default length of 12 with the a-z, A-Z, 0-9 character set returns
a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
"""
if not using_sysrandom:
# This is ugly, and a hack, but it makes things better than
# the alternative of predictability. This re-seeds the PRNG
# using a value that is hard for an attacker to predict, every
# time a random string is required. This may change the
# properties of the chosen random sequence slightly, but this
# is better than absolute predictability.
random.seed(
hashlib.sha256(
('%s%s%s' % (random.getstate(), time.time(), settings.SECRET_KEY)).encode()
).digest()
)
return ''.join(random.choice(allowed_chars) for i in range(length))

返回的是一个随机的字符串

    # 接上面 def _sanitize_token
elif len(token) == CSRF_TOKEN_LENGTH:
return token
elif len(token) == CSRF_SECRET_LENGTH:
# Older Django versions set cookies to values of CSRF_SECRET_LENGTH
# alphanumeric characters. For backwards compatibility, accept
# such values as unsalted secrets.
# It's easier to salt here and be consistent later, rather than add
# different code paths in the checks, although that might be a tad more
# efficient. # 较旧的Django版本将cookie设置为CSRF_SECRET_LENGTH字母数字字符的值。 为了向后
# 兼容,接受诸如无保密秘密之类的值。这里更容易加盐并在以后保持一致,而不是在检查
# 中添加不同的代码路径,尽管这可能会更有效。
return _salt_cipher_secret(token)
return _get_new_csrf_token()

CsrfViewMiddleware.process_view

# django/middleware/csrf.py
class CsrfViewMiddleware(MiddlewareMixin):
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None # Wait until request.META["CSRF_COOKIE"] has been manipulated before
# bailing out, so that get_token still works # 如果装饰器 @csrf_exempt 生效,则不处理
if getattr(callback, 'csrf_exempt', False):
return None # Assume that anything not defined as 'safe' by RFC7231 needs protection
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if getattr(request, '_dont_enforce_csrf_checks', False):
# 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(). # 关闭CSRF检查测试套件的机制。在创建CSRF cookie之后,所以
# 其他所有内容继续完全相同(例如发送cookie等),但在调用
# reject()的任何分支之前。 return self._accept(request)

    def _accept(self, request):

        # Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None

接上面CsrfViewMiddleware.process_view的代码

            # is_secure 如果请求是安全的,返回True,意味着发出的是HTTPS请求。
if request.is_secure():
referer = request.META.get('HTTP_REFERER')
if referer is None:
return self._reject(request, REASON_NO_REFERER)
# _reject就是csrf验证不通过,因为reffer为空

返回一个丑拒的代码

    def _reject(self, request, reason):
logger.warning(
'Forbidden (%s): %s', reason, request.path,
extra={
'status_code': 403,
'request': request,
}
)
return _get_failure_view()(request, reason=reason)
                referer = urlparse(referer)

                # referer.scheme: 请求的协议,一般为http或者https
# referer.netloc: host域名 # 确保我们有一个有效的url在Referer中.
if '' in (referer.scheme, referer.netloc):
return self._reject(request, REASON_MALFORMED_REFERER) # Ensure that our Referer is also secure.
if referer.scheme != 'https':
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 = (
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() # 在这里,我们生成所有可接受的HTTP引用的列表,包括当前主机,因
# 为它已在上游验证。
# CSRF_TRUSTED_ORIGINS global_settings.py里为空的list,设置可
# 以信任的来源
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) csrf_token = request.META.get('CSRF_COOKIE')
if csrf_token is None:
# 没有CSRF cookie。对于POST请求,我们坚持使用CSRF
# cookie,这样我们就可以避免所有CSRF攻击,包括登录CSRF。
return self._reject(request, REASON_NO_CSRF_COOKIE) # Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
try:
# request.POST.get() 相当于获取request.POST['csrfmiddlewaretoken']的值,
# 若果出错就返回 ''.这里的csrfmiddlewaretoken是提交的表单中的值,在
# 模板中用{% csrf_token %} 生成
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). # 在我们完成读取POST数据之前处理断开的连接。
# process_view不应该引发任何exception,因此我们将忽略并返回403
#(假设他们仍在监听,他们可能不是因为错误)。 pass if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE.
# ajax中适用'X-CSRFToken'
# CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '') request_csrf_token = _sanitize_token(request_csrf_token)
# 对比两个csrf_token,一个是表单里隐藏的csrfmiddlewaretoken
#(或者ajax的hearder: X_CSRFTOKEN),另一个是自带的cookies里的csrf_token
if not _compare_salted_tokens(request_csrf_token, csrf_token):
# 匹配不对就拒绝
return self._reject(request, REASON_BAD_TOKEN) return self._accept(request)

  def _compare_salted_tokens(request_csrf_token, csrf_token):

    # Assume both arguments are sanitized -- that is, strings of
# length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
return constant_time_compare(
_unsalt_cipher_token(request_csrf_token),
_unsalt_cipher_token(csrf_token),
)
def _unsalt_cipher_token(token):
    """
Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length
CSRF_TOKEN_LENGTH, and that its first half is a salt), use it to decrypt
the second half to produce the original secret.
"""
salt = token[:CSRF_SECRET_LENGTH]
token = token[CSRF_SECRET_LENGTH:]
chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt))
secret = ''.join(chars[x - y] for x, y in pairs) # Note negative values are ok
return secret
    def _accept(self, request):
# Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None

get_token(重要)

get_token是在外部调用,由 Template 中的{% csrf_token %} 触发,由request的cookie不同做出不同的反应。

def get_token(request):
if "CSRF_COOKIE" not in request.META:
# 如果request中不存在csrf,先生成一个新的secret,加密赋值到META["CSRF_COOKIE"] 中,
# 后面用来放到set_cookie之中
csrf_secret = _get_new_csrf_string()
request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
else:
# 如果request的cookie中存在了csrf_token,冲洗解密,取出secret csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
request.META["CSRF_COOKIE_USED"] = True
# 返回另外一个加密生成的secret, 由于加密是随机的,所以与上面的META["CSRF_COOKIE"]不一样
return _salt_cipher_secret(csrf_secret)

上面返回的一个加密的secret将会被填充进入 
<input type="hidden" name="csrfmiddlewaretoken" value="{}" >value里面,随着表单一起提交并和cookie之中的csrf_token比较。

CsrfViewMiddleware.process_response

    def process_response(self, request, response):
if not getattr(request, 'csrf_cookie_needs_reset', False):
if getattr(response, 'csrf_cookie_set', False):
return response if not request.META.get("CSRF_COOKIE_USED", False):
return response # Set the CSRF cookie even if it's already set, so we renew
# the expiry timer.
self._set_token(request, response)
response.csrf_cookie_set = True
return response
    # 设置token
def _set_token(self, request, response):
if settings.CSRF_USE_SESSIONS:
request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
else:
response.set_cookie(
settings.CSRF_COOKIE_NAME,
# request.META['CSRF_COOKIE']就是在上面赋值的
request.META['CSRF_COOKIE'],
max_age=settings.CSRF_COOKIE_AGE,
domain=settings.CSRF_COOKIE_DOMAIN,
path=settings.CSRF_COOKIE_PATH,
secure=settings.CSRF_COOKIE_SECURE,
httponly=settings.CSRF_COOKIE_HTTPONLY,
)
# Set the Vary header since content varies with the CSRF cookie.
patch_vary_headers(response, ('Cookie',))

总结

  • 第一次访问页面

    • 首先第一次访问页面,Template中的{% csrf_token %}会启动get_token(不是私有方法_get_token),生产一个csrf_secret的值。
    • 这个值在_salt_cipher_secret中随机生产一个与csrf_secret长度相同的salt,利用salt加密csrf_secret,两个字符串拼接形成csrf_token,request.META['CSRF_COOKIE'] = csrf_token 并设置到cookie里面。
    • get_token返回的用随机生成的另外一个salt加密csrf_secret,同样拼接返回放入隐藏的input之中
  • 向页面提交表单 
    • 提交的cookie中含有的csrf_token与表单提交的csrfmiddlewaretokenprocess_view进行解密,比对,如果解密出来的数值不同直接返回_reject() 

django中间件CsrfViewMiddleware源码分析,探究csrf实现的更多相关文章

  1. Django中间件CsrfViewMiddleware源码分析

    Django Documentation csrf保护基于以下: 1, 一个CSRF cookie基于一个随机生成的值,其他网站无法得到,次cookie有CsrfViewMiddleware产生.它与 ...

  2. Django中间件部分源码分析

    中间件源码分析 中间件简介 中间件是一个用来处理Django的请求和响应的框架级别的钩子.它是一个轻量.低级别的插件系统,用于在全局范围内改变Django的输入和输出.每个中间件组件都负责做一些特定的 ...

  3. Django搭建及源码分析(三)---+uWSGI+nginx

    每个框架或者应用都是为了解决某些问题才出现旦生的,没有一个事物是可以解决所有问题的.如果觉得某个框架或者应用使用很不方便,那么很有可能就是你没有将其使用到正确的地方,没有按开发者的设计初衷来使用它,当 ...

  4. Django如何启动源码分析

    Django如何启动源码分析 启动 我们启动Django是通过python manage.py runsever的命令 解决 这句话就是执行manage.py文件,并在命令行发送一个runsever字 ...

  5. Django之DRF源码分析(二)---数据校验部分

    Django之DRF源码分析(二)---数据校验部分 is_valid() 源码 def is_valid(self, raise_exception=False): assert not hasat ...

  6. Django rest framework源码分析(一) 认证

    一.基础 最近正好有机会去写一些可视化的东西,就想着前后端分离,想使用django rest framework写一些,顺便复习一下django rest framework的知识,只是顺便哦,好吧. ...

  7. django 之(二) --- 源码分析

    CBV类视图继承 CBV:继承自View:注册的时候使用的as_view() 入口 不能使用请求方法的名字作为参数的名字 只能接受已经存在的属性对应的参数 定义了一个view 创建了一个类视图对象 保 ...

  8. 开源分布式数据库中间件MyCat源码分析系列

    MyCat是当下很火的开源分布式数据库中间件,特意花费了一些精力研究其实现方式与内部机制,在此针对某些较为重要的源码进行粗浅的分析,希望与感兴趣的朋友交流探讨. 本源码分析系列主要针对代码实现,配置. ...

  9. Django rest framework源码分析(3)----节流

    目录 Django rest framework(1)----认证 Django rest framework(2)----权限 Django rest framework(3)----节流 Djan ...

随机推荐

  1. Navicat for MySQL导入文件

    1.导入SQL文件超出Navicat限制时,需要设置其限制的大小(具体看SQL文件大小) 打开Navicat For MySQL的命令行界面,输入: set global max_allowed_pa ...

  2. C#队列Queue,利用队列处理订单

    一.什么是队列 队列(Queue)代表了一个先进先出的对象集合.当您需要对各项进行先进先出的访问时,则使用队列.当您在列表中添加一项,称为入队,当您从列表中移除一项时,称为出队. 这是摘抄网上的.做了 ...

  3. java compiler没有1.8怎么办

    选择第一个点击安装,安装完成后,重启eclipse,打开java compiler 就可以选择1.8了. 成功:  扫个红包吧! Donate捐赠 如果我的文章帮助了你,可以赞赏我 1 元,让我继续写 ...

  4. 怎样解决putty终端乱码的方法

    原文地址:https://jingyan.baidu.com/article/3aed632e5f00ae701080913a.html?qq-pf-to=pcqq.c2c 终端输入:echo $LA ...

  5. winform datagridview 如何设置datagridview隔行变色

    如何设置隔行变色. 如图:

  6. 单步调试理解webpack里通过require加载nodejs原生模块实现原理

    在webpack和nodejs里,我们经常使用require函数加载原生模块或者开发人员自定义的模块. 原生模块的加载,比如: const path = require("path" ...

  7. 【转载】#349 - The Difference Between Virtual and Non-Virtual Methods

    In C#, virtual methods support polymorphism, by using a combination of the virtual and override keyw ...

  8. 解决Wamp各版本中 Apache 文件列表图标无法显示

    Edit the following file manually and change the path to the icons folder (it appears times in the fi ...

  9. 自定义报告,用Java写一个html文件

    因为testng不满足我们的展示,所以我们会自己定义一个报告,实施步骤是,把静态页面做好后,放在Java项目中,其实和生成一个日志文件类似,只是该了后缀,Java中需要使用到PrintStream,案 ...

  10. @RequestMapping,@ResponseBody,@RequestBody用法

    本文转载:http://blog.csdn.net/ff906317011/article/details/78552426 1.@RequestMapping 国际惯例先介绍什么是@RequestM ...