在Flask项目中,如果报下面这种错,就是csrf_token的设置有问题,没有检验通过
The CSRF token is missing.
The CSRF session token is missing.
这两个都是源码中抛出的 ValidationError
Flask中请求体的请求开启CSRF保护可以按以下配置
1 | from flask import Flask |
思路分析
根据cfrf_token 校验原理,具体的步骤如下:
- 后端根据SECRET_KEY生成 csrf_token 的值,在服务器的session存一份未加密的csrf_token值。并且在前端请求的时候,将加密后的 csrf_token 传给前端,传 csrf_token 的方式大致有以下两种:
- a. 在模板中的Form 表单中添加隐藏字段
- b. 将 csrf_token 使用 cookie 的方式传给前端
- 在前端发起请求时,在表单或者请求头中带上指定的csrf_token
- 后端在接受请求之后,取到前端发送的 csrf_token ,解密后与服务器session的 csrf_token 的值进行比对
- 如果两者的值一致,则代表是正常的请求,否则可能是伪造的请求,不予通过
在 Flask 中,CSRFProtect 这个类专门只针对指定的 app 进行 csrf_token 校验,所有开发者需要做以下几件事情:
- 生成 csrf_token 的值(通过 generate_csrf() )
- 将 csrf_token 的值传给前端
- 在前端请求需带上 csrf_token
代码逻辑具体分析
后端要保证每次涉及 `['POST', 'PUT', 'PATCH', 'DELETE'] `这四种请求时,都要进行 csrf_token 校验,但是如果 view_func 非常多,每个都要写一次就太麻烦了。
解决思路:
1 | # flask.ext.wtf.csrf中有提供生成 csrf_token 值的函数,可以直接导入使用 |
在前端请求时带上 csrf_token
非表单请求,以ajax为例
1
2
3
4
5
6
7
8
9
10
11
12$.ajax({
...
// 需要设置请求头
headers:{'X-CSRFToken':getCookie('csrf_token')},
....
})
// 自定义 getCookie 函数来拿到cookie中的 csrf_token 值
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
* 补充说明:请求头里字段名之所以要设置为 X-CSRFToken,是因为 Flask 源码中是这样要求的,下面是部分源码
1
2
3
app.config.setdefault(
'WTF_CSRF_HEADERS', ['X-CSRFToken', 'X-CSRF-Token']
)
* 可以看出,app加载请求头检索的字段名是 'X-CSRFToken' 或者 'X-CSRF-Token' ,所以这两种写法都可以
表单请求
如果模板中有表单,不需要做任何事。与之前一样:
1
2
3
4<form method="post">
{{ form.csrf_token }}
...
</form>但如果模板中没有表单,你仍需要 CSRF 令牌:
1
2
3<form method="post" action="/">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>如果使用
csrf_token()
函数来作为 token 的值, 你需要设置X-CSRFToken
请求头。
Flask csrf_token 校验的源码剖析
下面的代码都是 Flask 中的部分源码
1 | if request.method not in current_app.config['WTF_CSRF_METHODS']: |
校验 csrf_token 的值
第一步,根据 field_name 拿到 csrf_token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23def _get_csrf_token(self):
# find the ``csrf_token`` field in the subitted form
# if the form had a prefix, the name will be
# ``{prefix}-csrf_token``
field_name = current_app.config['WTF_CSRF_FIELD_NAME']
# 先遍历表单
for key in request.form:
if key.endswith(field_name):
csrf_token = request.form[key]
if csrf_token:
return csrf_token
# 没有遍历到表单,遍历请求请求头
for header_name in current_app.config['WTF_CSRF_HEADERS']:
csrf_token = request.headers.get(header_name)
if csrf_token:
return csrf_token
# 都没有,直接返回 None
return None第二步,比对前端的 csrf_token 和 服务器 session 的csrf_token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45def validate_csrf(data, secret_key=None, time_limit=None, token_key=None):
# 拿到配置中设置的 secret_key
secret_key = _get_config(
secret_key, 'WTF_CSRF_SECRET_KEY', current_app.secret_key,
message='A secret key is required to use CSRF.'
)
# 拿到服务器 session 中 csrf_token 值对应的 key
field_name = _get_config(
token_key, 'WTF_CSRF_FIELD_NAME', 'csrf_token',
message='A field name is required to use CSRF.'
)
# 拿到 session 的有效期,默认3600秒
time_limit = _get_config(
time_limit, 'WTF_CSRF_TIME_LIMIT', 3600, required=False
)
# 如果没拿到 请求头里的 csrf_token,则抛出 The CSRF token is missing.
# data 是第一步得到的前端传过来的 csrf_token 值,是加密的
if not data:
raise ValidationError('The CSRF token is missing.')
# 判断服务器的 session 中是否有 csrf_token 这个 key,
# 没有就抛出 The CSRF session token is missing.
if field_name not in session:
raise ValidationError('The CSRF session token is missing.')
# 根据 secret_key 生成一个URL安全序列化对象 s
s = URLSafeTimedSerializer(secret_key, salt='wtf-csrf-token')
try:
# 用 s 对象的 loads 方法,对前端加密的 csrf_token 进行解密,
# 生成一个未加密的 csrf_token 值
token = s.loads(data, max_age=time_limit)
except SignatureExpired:
raise ValidationError('The CSRF token has expired.')
except BadData:
raise ValidationError('The CSRF token is invalid.')
# 最后,比对解密前端的 csrf_token 的值,是否与服务器 session 中未加密的 csrf_token 一致
# 一致,则通过校验,不一致,可能是伪造请求,抛出 The CSRF tokens do not match.
if not safe_str_cmp(session[field_name], token):
raise ValidationError('The CSRF tokens do not match.')