Falsk项目cookie中的 csrf_token 和表单中的 csrf_token实现

在Flask项目中,如果报下面这种错,就是csrf_token的设置有问题,没有检验通过

The CSRF token is missing.

The CSRF session token is missing.

这两个都是源码中抛出的 ValidationError

Flask中请求体的请求开启CSRF保护可以按以下配置

1
2
3
4
5
6
7
8
9
10
from flask import Flask
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)

app.config.from_object(Config)

CSRFProtect(app)

但CSRFProtect只做验证工作,cookie中的 csrf_token 和表单中的 csrf_token 需要按以下方式实现

思路分析

根据cfrf_token 校验原理,具体的步骤如下:
  1. 后端根据SECRET_KEY生成 csrf_token 的值,在服务器的session存一份未加密的csrf_token值。并且在前端请求的时候,将加密后的 csrf_token 传给前端,传 csrf_token 的方式大致有以下两种:
    • a. 在模板中的Form 表单中添加隐藏字段
    • b. 将 csrf_token 使用 cookie 的方式传给前端
  2. 在前端发起请求时,在表单或者请求头中带上指定的csrf_token
  3. 后端在接受请求之后,取到前端发送的 csrf_token ,解密后与服务器session的 csrf_token 的值进行比对
  4. 如果两者的值一致,则代表是正常的请求,否则可能是伪造的请求,不予通过
在 Flask 中,CSRFProtect 这个类专门只针对指定的 app 进行 csrf_token 校验,所有开发者需要做以下几件事情:
  1. 生成 csrf_token 的值(通过 generate_csrf() )
  2. 将 csrf_token 的值传给前端
  3. 在前端请求需带上 csrf_token

代码逻辑具体分析

后端要保证每次涉及  `['POST', 'PUT', 'PATCH', 'DELETE'] `这四种请求时,都要进行 csrf_token 校验,但是如果 view_func 非常多,每个都要写一次就太麻烦了。
解决思路:
  1. 使用请求钩子拦截响应,统一设置 csrf_token
1
2
3
4
5
6
7
8
9
10
# flask.ext.wtf.csrf中有提供生成 csrf_token 值的函数,可以直接导入使用
from flask.ext.wtf.csrf import generate_csrf

@app.after_request
def after_request(response):
# 通过generate_csrf()生成csrf_token的值
csrf_token = generate_csrf()
# 设置到cookie中
response.set_cookie("csrf_token", csrf_token)
return response
  1. 在前端请求时带上 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. 校验请求方式,请求方式是 ['POST', 'PUT', 'PATCH', 'DELETE']才会校验,否则直接 return
1
2
if request.method not in current_app.config['WTF_CSRF_METHODS']:
return
  1. 校验 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
      23
      def _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
      45
      def 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.')
------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%