Django作为一个重量级框架,其已经事先为我们内置了很多安全模块,CSRF中间件就是其中之一。那么究竟什么是CSRF,其中间件工作原理是怎样的,我们又应该如何去使用CSRF呢。这一篇文章我们一起来深入学习一下。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
文章目录
什么是CSRF
CSRF,全程Cross Site Request Forgery,中文名“跨站请求伪造”。
前面学习三种会话技术的时候,我们了解了cookie和session(session也借助于cookie)。假设用户小付在某网站www.alotofmoney.com
已经经过认证获得了cookie或者session存储在本地浏览器,此时坏人发给小付一个网址,里面有个链接,点击以后会访问www.alotofmoney.com
的一个汇款API。因为已经经过了认证,所以受害者小付此时用刚才同样的浏览器访问这个API是会成功的,即使这并不是他的本意。
这种利用已经被服务器信任的浏览器,伪造用户请求的攻击方式,就叫CSRF。
CSRF的防范策略
目前有下面三种方式可以用来防范CSRF:
- 利用token
既然存储在浏览器中的cookie那么轻易被利用,那么就不用浏览器中的cookie了,利用本地数据库或者文件中记录的token信息来访问网站。关于token的使用可以参考我的另一篇博客《【Django 014】Django2.2会话技术之Token》
- 确认referer
在网页跳转的时候,request头中会带有一个referer,在处理敏感请求的时候,referer应该和目标url属于同一个域名。而CSRF中的referer却是坏人自己的网站,所以会失败。但是类似爬虫可以修改请求header一样,难保坏人不去手动修改这个字段。所以这个检测手段简单但并不一定可靠。
- 给网页添加特殊cookie
这是Django中使用的方法。可以在网页中添加一些代码,使得该网页在发送请求的时候会带上一些特殊cookie,这个cookie在服务端可以被验证。如果验证通过标明是受信任的网页,反之则访问被拒绝。
Django中的CSRF中间件
下面重点来看看Django中实现CSRF的中间件。
新建的项目就会自带下面的这个中间件
'django.middleware.csrf.CsrfViewMiddleware',
点进去看看源码。
类的结构
先看下类大概长啥样
和所有中间件一样,继承自MiddlewareMixin
,是一个类装饰器。除了魔术方式以外一共还有7个方法
- process_request
- process_view
- process_response
这是我们前面说的5个切点中的3个,前两个在请求阶段,最后是在返回阶段。
- _accept
- _reject
- _get_token
- _set_token
这4个方法用来被上面3个切点调用,完成特定功能。以单下划线开头,表示不建议被外部访问。
下面就从数据流的方向依次看看这3个切点完成的功能。
process_request
其代码如下
def process_request(self, request):
csrf_token = self._get_token(request)
if csrf_token is not None:
# Use same token next time.
request.META['CSRF_COOKIE'] = csrf_token
本质就是从请求中拿到一个csrf_token
的值,如果能拿到,放到请求的元数据中供后面使用。
重点看下用来获取这个csrf_token
的函数_get_token
def _get_token(self, request):
if settings.CSRF_USE_SESSIONS:
try:
return request.session.get(CSRF_SESSION_KEY)
except AttributeError:
raise ImproperlyConfigured(
'CSRF_USE_SESSIONS is enabled, but request.session is not '
'set. SessionMiddleware must appear before CsrfViewMiddleware '
'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
)
else:
try:
cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
except KeyError:
return None
csrf_token = _sanitize_token(cookie_token)
if csrf_token != cookie_token:
# Cookie token needed to be replaced;
# the cookie needs to be reset.
request.csrf_cookie_needs_reset = True
return csrf_token
这里分了两种情况,if
结构下是利用session来获取,else
结构下是利用cookie来获取。
这里的两个配置项settings.CSRF_USE_SESSIONS
和settings.CSRF_COOKIE_NAME
是在全局配置文件django/conf/global_settings.py
里面定义的
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_USE_SESSIONS = False
所以这个函数的作用就是从cookie中获取csrftoken
这个key对应的值,但即使没获取成功也不会影响后续步骤。
process_view
其代码如下
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
if getattr(callback, 'csrf_exempt', False):
return None
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if getattr(request, '_dont_enforce_csrf_checks', False):
return self._accept(request)
if request.is_secure():
referer = request.META.get('HTTP_REFERER')
if referer is None:
return self._reject(request, REASON_NO_REFERER)
referer = urlparse(referer)
if '' in (referer.scheme, referer.netloc):
return self._reject(request, REASON_MALFORMED_REFERER)
# Ensure that our Referer is also secure.
if referer.scheme != 'https':
return self._reject(request, REASON_INSECURE_REFERER)
good_referer = (
settings.SESSION_COOKIE_DOMAIN
if settings.CSRF_USE_SESSIONS
else settings.CSRF_COOKIE_DOMAIN
)
if good_referer is not None:
server_port = request.get_port()
if server_port not in ('443', '80'):
good_referer = '%s:%s' % (good_referer, server_port)
else:
try:
# request.get_host() includes the port.
good_referer = request.get_host()
except DisallowedHost:
pass
good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
if good_referer is not None:
good_hosts.append(good_referer)
if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
reason = REASON_BAD_REFERER % referer.geturl()
return self._reject(request, reason)
csrf_token = request.META.get('CSRF_COOKIE')
if csrf_token is None:
return self._reject(request, REASON_NO_CSRF_COOKIE)
# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
try:
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
except IOError:
pass
if request_csrf_token == "":
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
request_csrf_token = _sanitize_token(request_csrf_token)
if not _compare_salted_tokens(request_csrf_token, csrf_token):
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
这里主要是三个大的if
结构,然后第三个if
又包含很多小的if
。
第一个if
结构如下
if getattr(request, 'csrf_processing_done', False):
return None
如果进来的请求带有csrf_processing_done
的属性,则成功通过。这算是一个比较宽泛的豁免csrf认证的方式,只需要将某些进来的请求手动添加该属性即可达到豁免目的。
第二个if
结构如下
if getattr(callback, 'csrf_exempt', False):
return None
这里是针对单个view函数的豁免,如果某个view函数带有csrf_exempt
属性,则请求该view函数时得到豁免。
第三个if
结构是指不在('GET', 'HEAD', 'OPTIONS', 'TRACE')
这四种请求方式中的操作,目前主要就是针对POST
方式。里面又包含了三大块。
第一块如下
if getattr(request, '_dont_enforce_csrf_checks', False):
return self._accept(request)
如果进来的请求带有_dont_enforce_csrf_checks
属性,可以得到豁免。
第二块是针对HTTPS
请求的额外处理,利用查看referer
的方式来进行验证,不满足特定referer
的话就直接会被拒绝,这里就不展开了。
重点是说一下最后一块。
第三块如下
csrf_token = request.META.get('CSRF_COOKIE')
if csrf_token is None:
return self._reject(request, REASON_NO_CSRF_COOKIE)
# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
try:
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
except IOError:
pass
if request_csrf_token == "":
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
request_csrf_token = _sanitize_token(request_csrf_token)
if not _compare_salted_tokens(request_csrf_token, csrf_token):
return self._reject(request, REASON_BAD_TOKEN)
首先,获取process_request
中拿到的cookie赋值给csrf_token
,假如前面拿到的是None,直接被拒掉。
然后如果是POST
请求,还会查看传递上来的数据中的csrfmiddlewaretoken
对应的值,赋值给request_csrf_token
。如果没有这个值,就尝试从元数据中获取。
最后对csrf_token
和request_csrf_token
做某种事先规定好的判定,不符合条件就会被拒绝。
所以这里一共交代了下面三件事:
- request级别的豁免方式
- view函数级别的豁免方式
- 针对
POST
方式,不仅需要带csrftoken
这个cookie,还有传上来csrfmiddlewaretoken
这个值得内容,并且这两者还要满足既定的关系才可以
待会我们就针对豁免和成功通过csrf来分别进行验证。
process_response
其代码如下
def process_response(self, request, response):
if not getattr(request, 'csrf_cookie_needs_reset', False):
if getattr(response, 'csrf_cookie_set', False):
return response
if not request.META.get("CSRF_COOKIE_USED", False):
return response
self._set_token(request, response)
response.csrf_cookie_set = True
return response
csrf得cookie有一定的更新周期,这里不展开说。这里会对需要设置或者更新cookie的情况进行处理,然后返回。
实际操作
还是以上一节《【Django 023】中间件Middleware(二):结合session和cache实现反爬虫中间件图文详解》中的那个登录页面为例,因为涉及到POST
请求,所以会走csrf验证。
不加任何处理的post请求
在login.html
的form表单中,去掉处理csrf的标签{% csrf_token %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="{% url 'app:login' %}" method="post">
{# {% csrf_token %}#}
<label for="name">Name: </label><input type="text" id="name" name="name" placeholder="Your name">
<input type="submit">
</form>
</body>
</html>
之后输入用户名登录,出现下面的403返回页面
成功csrf认证的post请求
按照上面错误页面的提示,在发出post请求的form表单中恢复{% csrf_token %}
,之后的效果如下
可以看到加了这个标签之后,post请求不仅包含了csrftoken
的cookie,也上传了csrfmiddlewaretoken
的数据。根据上面的分析,这种请求是可以通过的。
view函数豁免
有的时候我们开发了一些公共的API,并不是那么敏感,就可以将该API进行豁免。
前面提到说如果这个view方法有csrf_exempt
属性的话,可以直接豁免。Djang为我们贴心的准备了一个单独的装饰器@csrf_exempt
用来添加这一属性。
一个测试用的API如下
def test_api(request):
if request.method == 'POST':
name = json.loads(request.body).get('name')
response = JsonResponse(data={'name': name, 'message': 'Welcome'})
else:
response = HttpResponse('Error')
return response
题外话,这里是为了用curl命令测试方便所以采用
request.body
而不用request.POST
来获取POST数据。具体可参见《Django获取curl传递POST数据失败解决方法》
之后用curl命令测试一下,发现出现csrf的403报错
[fuhx@testmachine ~]$ curl -X POST -d '{"name":"xiaofu"}' http://127.0.0.1:8000/app/test_api/
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="robots" content="NONE,NOARCHIVE">
<title>403 Forbidden</title>
...
...
假如加上了装饰器
@csrf_exempt
def test_api(request):
if request.method == 'POST':
name = json.loads(request.body).get('name')
response = JsonResponse(data={'name': name, 'message': 'Welcome'})
else:
response = HttpResponse('Error')
return response
结果如下
[fuhx@testmachine ~]$ curl -X POST -d '{"name":"xiaofu"}' http://127.0.0.1:8000/app/test_api/
{"name": "xiaofu", "message": "Welcome"}
成功通过了csrf认证。
request批量豁免
想要对符合条件的所有path的请求批量豁免,按照源码中的设定,就可以在自定义的中间件做一个预处理,给request加上csrf_processing_done
的属性即可。如下
if request.path.startswith('/app/test_api/'):
request.csrf_processing_done = True
此时去掉上面的@csrf_exempt
装饰器依然可以成功访问API
[fuhx@testmachine ~]$ curl -X POST -d '{"name":"james"}' http://127.0.0.1:8000/app/test_api/
{"name": "james", "message": "Welcome"}
总结
这一节我们通过学习系统自带的csrf中间件源码,巩固了Django中间件的设计思路。同时学习了如下四种跳过csrf报错的方法:
- 注释掉csrf中间件,简单粗暴
- 在发送POST请求的form表单中添加
{% csrf_token %}
标签,完整通过csrf验证 - 对需要豁免的view函数添加
@csrf_exempt
装饰器 - 对批量的request请求添加
csrf_prcessing_done
属性
出于安全考虑,除了公共API可以豁免外,还是建议在自己的应用中加入csrf认证。