本文要说明一个问题: Django 后端什么时候会让前端在 cookie 中的写 crsftoken? 是每次请求都会写一个新的 crsftoken 吗?
C:\Python27\Lib\site-packages\django\middleware\csrf.py
def _sanitize_token(token):
# Allow only alphanum
if len(token) > CSRF_KEY_LENGTH:
return _get_new_csrf_key()
token = re.sub('[^a-zA-Z0-9]+', '', force_text(token))
if token == "":
# In case the cookie has been truncated to nothing at some point.
return _get_new_csrf_key()
return token
class CsrfViewMiddleware(object):
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
try:
csrf_token = _sanitize_token(
request.COOKIES[settings.CSRF_COOKIE_NAME])
# Use same token next time
request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
csrf_token = None
# Generate token and store it in the request, so it's
# available to the view.
request.META["CSRF_COOKIE"] = _get_new_csrf_key()
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
# bailing out, so that get_token still works
if getattr(callback, 'csrf_exempt', False):
return None
# Assume that anything not defined as 'safe' by RFC2616 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().
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
# nonce 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 = force_text(
request.META.get('HTTP_REFERER'),
strings_only=True,
errors='replace'
)
if referer is None:
return self._reject(request, REASON_NO_REFERER)
# Note that request.get_host() includes the port.
good_referer = 'https://%s/' % request.get_host()
if not same_origin(referer, good_referer):
reason = REASON_BAD_REFERER % (referer, good_referer)
return self._reject(request, reason)
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 = ""
if request.method == "POST":
try:
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('HTTP_X_CSRFTOKEN', '')
if not constant_time_compare(request_csrf_token, csrf_token):
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
def process_response(self, request, response):
if getattr(response, 'csrf_processing_done', False):
return response
# If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was
# never called, probably because a request middleware returned a response
# (for example, contrib.auth redirecting to a login page).
if request.META.get("CSRF_COOKIE") is None:
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.
response.set_cookie(settings.CSRF_COOKIE_NAME,
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
)
# Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',))
response.csrf_processing_done = True
return response
流程说明:
有了上面的基础,咱们来尝试回答上面提到的问题:是不是每次请求都会在前端浏览器的 cookie 中写一个 crsftoken 呢?
答案:明显不是,因为 process_response 中有:
if not request.META.get("CSRF_COOKIE_USED", False):
return response
当请求头中没有 CSRF_COOKIE_USED 时,就会直接返回。而一般的请求头中是没有的,所以一般的请求是不会在前端浏览器的 cookie 中写一个 crsftoken 的。
那么何时会写呢?
当你在请求头中设置 CSRF_COOKIE_USED 为 True 时,上面的 if 就不成立了,这样流程就会走到 response.set_cookie()。经搜索 CSRF_COOKIE_USED 发现, rotate_token 函数会在请求头中设置 CSRF_COOKIE_USED 为 True, 而 rotate_token 会被 login 调用,至此答案已经明确了。
发散个上面流程说明提到的 3:
当访问登录 URL 时,HTTP 的请求方法是 GET 时,会在前端浏览器的 cookie 中写入 crsftoken。为了好描述咱们暂且称之为 tokeA,该 Token 的写入猜测是在 render 流程中触发的,该值和 Form 表单中的隐藏元素 csrfmiddlewaretoken 的值一样。当用户在登录表单中输入用户名和密码后,以 POST 请求登录 URL 时,后端的视图函数一般会做登录操作即调用 login() 函数,那么也就是说当这个 POST 请求的生命周期走到 django.middleware.csrf.CsrfViewMiddleware 的 process_response 时,会触发重新 在前端浏览器的 cookie 中写一个 crsftoken 的暂且称之为 tokenB。
可知 tokenA 和 tokenB 是不一样的,也就是说:对于登录 URL,GET 请求时会在前端浏览器的 cookie 中写一个 crsftoken(tokenA), POST 请求时会在前端浏览器的 cookie 中写另一个 crsftoken(tokenB),登录成功后后面访问其它 URL 的请求都会携带着这个 POST 得到的 crsftoken (tokenB)
扩展1(详情参考robappdj项目的 mymiddleware):
猜测 crsf 是在中间件 CsrfViewMiddleware 的 process_request 中还是 process_view 中实现
答案是 process_view 中,因为 view 函数可以用装饰器 csrf_exempt 来免除 crsf 认证,
所以不能在 process_request 中实现,因为根据请求的 url 匹配视图函数
是在 中间件的 process_request 执行后 process_view 执行前进行的
CsrfViewMiddleware 的 process_view 做了两件事
a. 检查视图函数是否被 @csrf_exempt (免除 crsf 认证)
b. 去请求体或 cookie 中获取 crsftoken,然后完成校验
可以参考:Django请求的生命周期
扩展2:
DRF会避开 csrf 因为其类 APIView 继承了Django框架的 View 并重写了 as_view 函数。在 as_view 的最后
# Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt.
return csrf_exempt(view)
D:\WorkSpace\Archiver\archiver_gitcode\venv\Lib\site-packages\rest_framework\views.py
class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
# Allow dependency injection of other settings to make testing easier.
settings = api_settings
schema = DefaultSchema()
@classmethod
def as_view(cls, **initkwargs):
"""
Store the original class on the view function.
This allows us to discover information about the view when we do URL
reverse lookups. Used for breadcrumb generation.
"""
if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
def force_evaluation():
raise RuntimeError(
'Do not evaluate the `.queryset` attribute directly, '
'as the result will be cached and reused between requests. '
'Use `.all()` or call `.get_queryset()` instead.'
)
cls.queryset._fetch_all = force_evaluation
view = super(APIView, cls).as_view(**initkwargs)
view.cls = cls
view.initkwargs = initkwargs
# Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt.
return csrf_exempt(view)