Django默认提供了CSRF保护模块,在settings.py的MIDDLEWARE节加上"django.middleware.csrf.CsrfViewMiddleware"就可以自动开启,而且当使用django-admin startproject project_name时会自动添加,很方便。
传统上Django会使用template来处理html页面并返回给客户端,如果是这样,那么只有当客户端采用POST、DELETE、PUT、PATCH这4中非幂等的方法访问服务器时才会激活CSRF保护中间件。而如果采用模板得到的html,这4中方法都是在表单元素中才有。而处理起来也很简单,只要在template的表单元素中增加一句{% csrf_token %}就可以自动在表单中添加隐藏的csrftoken条目,并且在表单提交时自动把csrftoken的内容合并到请求中发给服务器,服务器接到的请求中找到了csrftoken并且比对成功,CSRF就通过了。
但是当使用Ajax时,没有表单,Django也没法自动添加相关内容,就需要开发者额外处理。
服务端发放csrftoken
一般来讲,在中间件中配置CSRF中间件,Django就会对所有的访问请求开启CSRF防护,但是当一个用户初次访问网站时是没有csrftoken的,这个时候就必须要由服务器发放一个csrftoken给客户端,也就是说这个发放csrftoken的功能——比如说登录功能——是必须要使用csrf_exempt关闭该功能的CSRF防护的。
然后在登录接口中需要使用
from django.conf import settings
from django.middleware.csrf import rotate_token, get_token
rotate_token(request) # 生成csrftoken的随机字符串
tokenstr = get_token(request) # 取出csrftoken字符串
response.set_cookie(
key=settings.CSRF_COOKIE_NAME,
value=tokenstr,
domain=settings.SESSION_COOKIE_DOMAIN,
)
return response
其中CSRF_COOKIE_NAME和SESSION_COOKIE_DOMAIN在settings.py中设置,具体设置方法参考
https://docs.djangoproject.com/zh-hans/4.1/ref/settings/#csrf-cookie-name
这样就可以把生成的csrftoken随机字符串通过cookie发送给客户端,cookie中可以通过CSRF_COOKIE_NAME把这个字符串读取出来。等到用户下次再访问的时候,只要把这个csrftoken通过合适的方式发送给服务器就可以让服务器的CSRF防护通过。
当然,也可以把csrftoken以其他形式加在响应内容中发给客户端,客户端根据前后端协商的接口对csrftoken做处理,等待下次访问时按照协商好的接口把csrftoken发送服务器做验证。
在
https://docs.djangoproject.com/zh-hans/4.1/howto/csrf/#using-csrf-protection-with-ajax
对把csrftoken存放在cookie中的安全性提出了一些质疑,不过只要调整CSRF_USE_SESSIONS设置和CSRF_COOKIE_HTTPONLY设置就可以调整好安全性。但是要记住,如果设置了CSRF_USE_SESSIONS,那么就不能通过cookie读取csrftoken。如果设置了CSRF_COOKIE_HTTPONLY就不能通过JavaScript读取csrftoken,就只能使用表单内嵌的隐藏元素来提交csrftoken了。
不管怎么说,只要浏览器设置正确,把发给客户端的csrftoken存放在cookie是安全的。
客户端取得csrftoken
客户端在发送Ajax请求之前,必须要通过JavaScript取得存在cookie里的csrftoken,比如说使用
https://github.com/js-cookie/js-cookie/
提供的js-cookie包就可以通过
import Cookies from 'js-cookie'
csrftoken_str=Cookies.get('csrftoken')
来方便的把csrftoken读取出来。
客户端把csrftoken按照符合规定的格式发送给服务器
在Header中发送
按照
How to use Django's CSRF protection | Django 文档 | Django
的说明,客户端在发送Ajax请求到服务器时需要按照以下格式设置HTTP请求的Header
'X-CSRFToken': csrftoken_str
这样就可以把csrftoken发送给服务器了,其中的key也就是X-CSRFToken来源于
https://docs.djangoproject.com/zh-hans/4.1/ref/settings/#csrf-header-name
的要求。value也就是csrftoken_str是前面JavaScript取出来的csrftoken值。
通常来讲这样服务器就可以接受了。但是实际上这里面还有浏览器做的2项额外的处理。
浏览器的额外处理
服务器同时服务大量的用户,每个用户都有一个csrftoken,所以服务器首先要知道请求者是谁,然后才能拿着这个请求中的csrftoken去对比。也就是说在提交csrftoken的同时,还要提交session的id值
这个session的id值是怎么发送给客户端的呢。类似csrftoken,这个sessionid通常也是在登录时有服务器生成,然后一并发送给客户端的。
在
说明在Django的settings.py中的SESSION_COOKIE_NAME配置了服务器在发送session的id值时采用的配置。也就是说在cookie中SESSION_COOKIE_NAME这个key对应的value就是session的id值。
通常来讲,浏览器在访问服务器时会默认把对应的域名的所有cookei都一并发送给服务器,所以这里是浏览器自动处理的,一般不需要开发者干预。但是如果需要额外干预的话,开发者可以用类似的方法把sessionid读取出来,然后在Request Header中增加相关内容发送给服务器
'sessionid':sessionid_value
另外,浏览器通常的cookie安全策略都是严格同源,也就是不同源的cookie浏览器默认是不会发送的,除非浏览器的用户做了特殊设置。同时服务器发给客户端的cookie也要设置安全策略:是否允许不同源的会话读取cookie。
https://docs.djangoproject.com/zh-hans/4.1/ref/settings/#csrf-cookie-samesite
有Django对cookie安全设置的说明,主要的两个选项是CSRF_COOKIE_SAMESITE和CSRF_COOKIE_SECURE
如果这里要求开发者手动干预的话,需要设置request选项
mode: 'same-origin' // Do not send CSRF token to another domain.
其实也就是在Request Header中增加了这么一个选项
如果是使用浏览器的话,这两项一般是不需要额外设置的。
但是如果是使用开发辅助工具模拟浏览器访问服务器的话——比如说PostMan,就需要手动设置一下。因为众所周知的原因,Google市场难以访问,所以我用Firefox浏览器上的模拟插件</>RESTD
RESTED – 下载 🦊 Firefox 扩展(zh-CN)
这样在访问有CSRF防护的url的时候,就需要在Header增加csrftoken、sessionid、mode这么3个选项才能正确访问。当然,对于Ajax访问而言,Content-Type也是必须要设置的。所以一共需要设置至少4个Header参数才能保证正确。
在request body中发送
在HTTP和HTTPS协议中,在Header中的信息都是不加密的,如果把一些保密信息比如sessionid/csrftoken这种涉及到身份认证的信息放在Header中如果被有心人抓包,是容易泄漏的。
在HTTPS协议中 ,POST的body是加密的,所以更安全的方法是把这些涉及到身份认证 的信息放在body里。
如果是用axios或者XMLHttpRequest的话,不需要任何处理。因为XMLHttpRequest默认会自动读取cookie,并在向服务器发送request的时候把cookie放在body里,而axios是对XMLHttpRequest的封装。除非手动对request body进行处理,否则cookie完全可以交给XMLHttpRequest自动处理。