Django 之前端获取 csrftoken 的几种方式以及源码解释(超详细)

表单提交时

<form action="", method="post">
    {% csrf_token %}
</form>

如果是前后端不分离的项目,django 为了防止 XSS 攻击,要求所有 form 表单提交时加上 {% csrf_token %} 标签,前端会再 html 中加入一个 input 标签 django.middleware.csrf.CsrfViewMiddleware 这个中间件就是来验证 csrf_token 如果没有加,就会出错,这个 token 每次刷新页面时都会刷新,客户在请求时。会再服务器端生成一个 token,然后放在 cookie里面,每次 post 请求都会带上这个 token,去和表单里面的去对比。

  1. 官⽅⽂档中说到,检验token时,只⽐较secret是否和cookie中的secret值⼀样,⽽不是⽐较整个token。

  2. token字符串的前32位是salt,后⾯是加密后的token,通过salt能解密出唯⼀的secret。

AJAX 提交时

使用表单时,必须在每个 post 上添加 csrf_token 标签,不够方便,处于这个原因,有一种替代方案,设置一个自定义的 X-CSRFToken 头(由 CSRF_HEADER_NAME 设置指定)为 CSRF 标记的值。这通常比较容易,因为许多 JavaScript 框架提供了钩子,允许在每个请求中设置头。
CSRF 令牌 cookie 默认命名为 csrftoken,但你可以通过 CSRF_COOKIE_NAME 配置来控制 cookie 的名称。
可以通过这样的方式获得 csrftoken:

function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
const csrftoken = getCookie('csrftoken');

后续可以使用这个函数来获取 csrftoken:

const csrftoken = Cookies.get('csrftoken');

在请求头中设置令牌:

const request = new Request(
    /* URL */,
    {
        method: 'POST',
        headers: {'X-CSRFToken': csrftoken},
        mode: 'same-origin' // Do not send CSRF token to another domain.
    }
);
fetch(request).then(function(response) {
    // ...
});

后续再请求时需要带上 X-CSRFToken 这个头,以postman 为例:
Django 之前端获取 csrftoken 的几种方式以及源码解释(超详细)_第1张图片

CsrfViewMiddleware 源码

想必都看到过这个报错:
Django 之前端获取 csrftoken 的几种方式以及源码解释(超详细)_第2张图片
这就是没有加 csrf_token ,要么是表单中没加,要么是没获取到 token
重点都在 django.middleware.csrf.CsrfViewMiddleware 这个中间件里面,导入之后点进去看。

# 从请求的cookie中获取csrftoken的值 
# 一般是一个键值对,key是 csrftoken  vlaue 是一串随机字符串
csrf_token = request.META.get('CSRF_COOKIE')
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.
    # 如果获取不到,返回:CSRF cookie not set. 就是没有设置 cookie 的意思
    return self._reject(request, REASON_NO_CSRF_COOKIE)

# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
    try:
    	# 从 post 里面获取 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).
        pass

# 这里比较重要了
if request_csrf_token == "":
    # Fall back to X-CSRFToken, to make things easier for AJAX,
    # and possible for PUT/DELETE.
    # 如果 request_csrf_token 里面没有,就去请求头中去拿 CSRF_HEADER_NAME 
    # 默认是 X_CSRFTOKEN, 名字也可以在 setting 中自己设置
    request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

request_csrf_token = _sanitize_token(request_csrf_token)
# 校验表单中的 token 和 cookie 中的token是不是一致
# 检验token时,只⽐较secret是否和cookie中的secret值⼀样
# 这个 secret 应该是配置文件中的 SECRET_KEY
if not _compare_salted_tokens(request_csrf_token, csrf_token):
	# 比较失败,返回一个页面,内容就是我们熟悉的:CSRF token missing or incorrect.
    return self._reject(request, REASON_BAD_TOKEN)

装饰器方法

csrf_protect : 强制执行 csrf 验证

from django.shortcuts import render
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def my_view(request):
    c = {}
    # ...
    return render(request, "a_template.html", c)

csrf_exempt:免除 csrf 的保护

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def my_view(request):
    return HttpResponse('Hello world')

对于 CSRF cookie 常用的一些全局变量

CSRF_COOKIE_NAME = 'csrftoken'  # 默认的 key 名称
CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52  # 存活时间
CSRF_COOKIE_DOMAIN = None  # 在那个域名下生效
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'  # 请求头的名称

CSRFTOKEN 比较方式源码

每个 django 版本可能稍有不同

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

参考文献

django 官网:https://docs.djangoproject.com/zh-hans/4.0/ref/csrf/
Django之同源和跨域、CSRF详解:https://blog.csdn.net/qq_39253370/article/details/105684890?spm=1001.2014.3001.5501

你可能感兴趣的:(python,orm,django,python,drf,restful,后端)