Django csrf 的源码解析(DRF会避开 csrf 因为其类 APIView 继承了Django框架的 View 并重写了 as_view 函数)

本文要说明一个问题: 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

流程说明:

  1. process_view -> _sanitize_token: 检查 token 的合法性; 如果 token 是空,则创建一个 token
  2. process_response 函数的最后 response.set_cookie 才会触发前端在 cookie 中的写 crsftoken
  3. 一个请求的生命周期:当访问登录 URL,HTTP 的请求方法是 GET 时, 后端的视图函数会 render -> HttpResponse 返回前端一个登录页面(其中含有登录的表单),这个登录表单中有个隐藏的元素 name=“csrfmiddlewaretoken”,同时由于请求会走到中间件django.middleware.csrf.CsrfViewMiddleware 的 process_response,所以会在前端浏览器的 cookie 中写入 crsftoken。

有了上面的基础,咱们来尝试回答上面提到的问题:是不是每次请求都会在前端浏览器的 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,然后完成校验


扩展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)
发布了44 篇原创文章 · 获赞 0 · 访问量 3958

猜你喜欢

转载自blog.csdn.net/cpxsxn/article/details/99659457