这个中间件主要是防止 CSRF 攻击的。
首先来总体概括一下:
csrf_token 是一个全局变量,是 django 自动生成的,
csrf_token
不需要 django 存储。每次刷新生成一个新的 token,然后将 token 返回给前端,同时伴随着 set-cookie 操作,把 token 写进浏览器 cookie。验证请求的时候,只需拿 cookie 里面的csrftoken
和 csrf_token 和提交表单里面的csrfmiddlewaretoken
值进行比较即可。如果一样,即请求有效。这里的比较不是看两个值是否相等,而是通过一系列解密判断的。
接着来看一看源码:
class CsrfViewMiddleware(MiddlewareMixin):
def _accept(self, request):
pass
def _reject(self, request, reason):
pass
def _get_token(self, request):
pass
def _set_token(self, request, response):
pass
def process_view(self, request, callback, callback_args, callback_kwargs):
pass
def process_response(self, request, response):
pass
主要是 process_view
和 process_response
,同样,view 主要是在 url 匹配之后,确定视图函数之前,而 response 是在执行完视图函数之后。接下来来看一下具体流程:
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
csrf_token = self._get_token(request)
if csrf_token is not None:
# Use same token next time.
request.META['CSRF_COOKIE'] = csrf_token
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 = force_text(
request.META.get('HTTP_REFERER'),
strings_only=True,
errors='replace'
)
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)
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:
# request.get_host() includes the port.
good_referer = request.get_host()
good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
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)
if csrf_token is None:
return self._reject(request, REASON_NO_CSRF_COOKIE)
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)
这里我们思考一下为什么实现了 process_view
呢?process_view
是在 匹配 url 后准备执行视图函数前才调用的。因为某些 url 对应的视图函数里不准备使用 csrf 验证,那么验证这步干什么呢?
首先判断 request
中有没有 csrf_processing_done
,这个是干什么的呢。然后判断回调函数时候是否有 csrf_exempt
,因为这个属性代表不需要 csrf token 验证。
然后 _get_token
里面判断有没有 token,有的话比对一下,否则返回 None。接着判断视图函数是否需要 csrf 防护,如果没有的话,当然也不用分析了。
接着判断 HTTP 方法,如果不是 ('GET', 'HEAD', 'OPTIONS', 'TRACE')
这几种,那么继续执行。因为 rfc 定义这几种方法都是安全的。然后关闭测试套件的 csrf 检查,这里不明白。
下来判断是否是 https
请求,这里进行了一些检查,说实话,这里也没看懂。
接下来就是去 request.META
去获取 CSRF_COOKIE
,如果没有则返回 403 页面。接着如果是 POST 请求就去 request.POST
中获取 csrfmiddlewaretoken
值,这里有可能会报错吗?如果没有,则会可能是其它请求或者是 AJAX 请求,则去 request.META
中去取,然后这里进行了 _sanitize_token
函数,主要是检查 request_csrf_token
是否符合规范
详情如下
def _sanitize_token(token):
# Allow only ASCII alphanumerics
if re.search('[^a-zA-Z0-9]', token):
return _get_new_csrf_token()
elif len(token) == CSRF_TOKEN_LENGTH:
return token
elif len(token) == CSRF_SECRET_LENGTH:
return _salt_cipher_secret(token)
return _get_new_csrf_token()
如果不合规,直接构造一个新的 csrf_token
,如果合规,返回原 csrf_token
。
最后比较两个值,一个是表单里面的,一个是请求头中的 cookie
值 csrf_token
。这里检验的话,有一个好办法,在浏览器中查看请求头和提交的参数。这里里面使用和 hmac
来验证。具体的可以跟踪看看。返回真就进入 _accept
函数,里面奇怪定义了equest.csrf_processing_done = True
,不解,望大佬赐教。如果不相等,则返回 403 页面。
下来看一看 process_response
:
def process_response(self, request, response):
self._set_token(request, response)
response.csrf_cookie_set = True
return response
这里是核心部分,至于检查那些参数,我也不是很懂。这里 _set_token
,然后进去 response.set_cookie
操作,设置 cookie 值为 csrftoken
。
好了,就到这里吧,里面还有一些细节没有掌握,例如 Django 随时在判断 request 和 response 的状态,我对状态还有一些不清楚,望大佬赐教。