众所周知,django通过CsrfViewMiddleware中间件来下发和校验csrf-token有效性的。
那么,这个中间到底是怎么实现csrf-token校验的呢?
- 首先,django 第一次响应来自某个客户端的请求时,会在服务器端随机生成一个 csrf-token,并把这个 csrf-token 放在 cookie 里。然后每次 POST 请求都会带上这个 csrftoken。(这个csrftoken的有效期非常长(52周))
- 在前后端不分离的django项目中,可以通过{%csrf_token%}标签在表单中自动生成csrf-token,当提交这个表单时,csrf-token一并被提交。
- 表单提交后,CsrfViewMiddleware中间件会自动校验cookie和表单中的csrftoken是否一致,一致则是合法请求。
那么,问题又来了:
- 我每次刷新表单页面时,表单中的csrftoken也会更新,而cookie中的csrftoken值却没变,由此服务器又是怎么校验的呢?
When validating the ‘csrfmiddlewaretoken’ field value, only the secret, not the full token, is compared with the secret in the cookie value. This allows the use of ever-changing tokens. While each request may use its own token, the secret remains common to all.
This check is done by CsrfViewMiddleware. --来自django官方文档
官方文档中说到,检验csrftoken时,只比较secret而不是比较整个token。
我又有疑问了,同一次登录,form表单中的token每次都会变,而cookie中的token不便,django把那个salt存储在哪里才能保证验证通过呢。直到看到源码。
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
token字符串的前32位是salt, 后面是加密后的token, 通过salt能解密出唯一的secret。
django会验证表单中的token和cookie中token是否能解出同样的secret,secret一样则本次请求合法。
同样也不难解释,为什么ajax请求时,需要从cookie中拿取token添加到请求头中。
贴一个前后端分离项目中如何实现csrf-token校验:
前后端不分离可以使用{% csrf_token%}标签生成token,前后端不分离时可以自定义一个获取csrftoken的函数:
from django.middleware.csrf import get_token
def getToken(request):
token = get_token(request)
return HttpResponse(json.dumps({'token': token}), content_type="application/json,charset=utf-8")
前端表单提交前,可以通过ajax获取token并写入header中:'X-CSRFtoken':csrftoken
或者更为简便的方法:https://blog.csdn.net/bocai_xiaodaidai/article/details/97375420