浅谈django项目中前后端分离csrf传递

前言

回想起来以后有很久没有用到django提供自带的模板功能了,目前公司的项目基本都采用react+web后端的模式,这样做也相对灵活但是也会出现一些问题,比如如何处理csrf以及登录认证的问题。

CSRF

在说之前,先整体屡一下csrf_token生成的过程。
首先在中间件的配置中加入django.middleware.csrf.CsrfViewMiddleware,没有这个的话就没有下面的一切了。
这个中间件在process_view中主要是用来生成csrf_token,上来会从cookie中获取,如果cookie中没有,会自动生成一个放到request.META[“CSRF_COOKIE”],源代码如下

1
2
3
4
5
6
7
8
9
10
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()

这里注意,如果是POST方法,且cookie中没有csrf_token的话,那后面的逻辑就会直接报403,提示csrf校验失败,所以上来必须是GET请求,让cookie先种下去才可以。
接下来看process_response,源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
)

程序会判断CSRF_COOKIE与CSRF_COOKIE_USED是否为空,CSRF_COOKIE这个不用多说,因为有前面process_view的生成,所以正常情况下都是存在的,但是CSRF_COOKIE_USED是怎么设置的呢?
答案在django.middleware.csrf.get_token中。这个函数就做了2个事情,一个是将CSRF_COOKIE_USED设置为True,一个是返回request.META.get(“CSRF_COOKIE”, None)。
所以如果你想生成csrf_token,前置条件是必须要调用get_token(或者rotate_token)。
总结一下:
1.必须调用get_token,只有调用它,返回才会设置set-cookie把csrf_token种进去
2.前端需要通过form表单或者HTTP_X_CSRFTOKEN头把csrf_token传到后端,然后后端用传过来的值和cookie的值比对,一致就通过。

接下来说说怎么与前端结合,因为传统模板的模式下,csrf_token会自动渲染进页面,js可以直接拿到页面的值,但是在前后端分离的情况下我们应该怎么做呢?
1.先在后端写一个生成csrf_token的接口

1
2
3
def get_token(request):
token = django.middleware.csrf.get_token(request)
return JsonResponse({'token': token})

2.前端去请求这个接口,拿到token,保存到cookie中,然后post请求需要csrf_token时,直接从cookie中获取即可
3.在后端的views中要加入ensure_csrf_cookie装饰器,这个装饰器的作用就是确保这次返回的时候,回把csrf_token再成功的种回去,刷新过期时间,其实如果不加的话并不影响校验。
中间件会去验证前端传过来的token和META cookie中的token是否一致,源码逻辑如下

1
2
3
4
5
6
7
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)

这样就可以完成前后端分离模式下的csrf_token的传递。

另外再多说一句登录认证的事情,因为前后端分离,在开发接口前端工程师通常和后端工程师不在一个域下面,所以session-cookie方式登录就不好使了,需要约定token来实现登录认证。
另外一种方式是jwt认证(JSON Web Tokens),前端用账号密码换取jwt,之后的所有请求都带着jwt,即可实现登录认证。