中间件和csrf
# 中间件介绍
中间件位于wsgiref与url路由层之间.
中间件使我们在视图函数执行之前和执行之后都可以做一些额外的操作
它本质上就是一个自定义类, 类中定义了几个方法, Django框架会在请求的特定的时间去执行这些方法!!
位置 -- Django项目的setting.py文件的MIDDLEWARE配置项!!
# -- 中间件是处理Django的request和response对象的钩子.
# 当用户在网站中进行单击某个按钮等操作时候,这个动作表明用户向网站发送请求(request);
# 而网页会根据用户的操作返回相应的网页内容,这个过程称为响应(response)处理.
# 当django接受到用户请求时,django会首先经过中间件进行处理请求信息,将处理结果返回给用户.返回的过程也会经过中间件!
MIDDLEWARE = [
# -- 内置的安全机制,保护用户与网站的通信安全
'django.middleware.security.SecurityMiddleware',
# -- 会话session功能
'django.contrib.sessions.middleware.SessionMiddleware',
# -- Django内置功能支持中文显示
'django.middleware.locale.LocaleMiddleware',
# -- 处理请求信息,规范化请求内容
'django.middleware.common.CommonMiddleware',
# -- 开启CSRF防护功能
'django.middleware.csrf.CsrfViewMiddleware',
# -- 开启内置的用户认证系统
'django.contrib.auth.middleware.AuthenticationMiddleware',
# -- 开启内置的信息提示功能
'django.contrib.messages.middleware.MessageMiddleware',
# -- 防止恶意程序点击劫持
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在pycharm中, ctrl+b跳转到CsrfViewMiddleware、SessionMiddleware等类中..
查看源码,发现这些 中间件相关的类都继承了MiddlewareMixin!!
中间件里可以定义5个方法:
1> process_request(self,request)
2> process_response(self, request, response)
3> process_view(self, request, view_func, view_args, view_kwargs)
4> process_template_response(self,request,response)
5> process_exception(self, request, exception)
暂且只需要关注前两个方法.. 其余3个方法的解释请参考文档: https://www.cnblogs.com/Dominic-Ji/p/9229509.html
# 自定义中间件
在Django根项目或应用下创建一个文件夹(这里我命令为middleware),并在该文件夹下创建一个py文件(my_middleware.py)
记得注册中间件
# 最原始的方式
class Mymd(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
print("来了")
# 执行路由对应的视图函数
response = self.get_response(request)
print("走了")
return response
"""
控制台打印:
来了
函数
走了
"""
MIDDLEWARE = [
... ...
'middleware.my_middleware.Mymd',
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 中间件的执行顺序
MIDDLEWARE = [
... ...
'middleware.my_middleware.M1',
'middleware.my_middleware.M2',
]
2
3
4
5
views.py
from django.shortcuts import HttpResponse
def index(request):
print("index.")
return HttpResponse('index')
2
3
4
5
my_middleware.py
from django.utils.deprecation import MiddlewareMixin
class M1(MiddlewareMixin):
def process_request(self, request): # -- 这个request和视图函数中的request是一样的!!
print("M1的process_request.")
def process_response(self, request, response):
print("M1的process_response.")
return response
class M2(MiddlewareMixin):
def process_request(self, request):
print("M2的process_request.")
def process_response(self, request, response):
print("M2的process_response.")
return response
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
从实验结果可以验证以下三个结论:
1> 中间件在路由之前
2> 请求来的时候 -- 从上往下的经过中间件.
3> 响应回去的时候 -- 从下往上的经过中间件.
# 拦截请求
修改一点代码,进行实验.
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class M1(MiddlewareMixin):
def process_request(self, request):
print("M1的process_request.")
# -- 不再让process_request默认返回None. 返回None,会按照正常流程走.
return HttpResponse('Hello World!!') # -- 修改的地方!!
def process_response(self, request, response):
print("M1的process_response.")
return response
class M2(MiddlewareMixin):
def process_request(self, request):
print("M2的process_request.")
def process_response(self, request, response):
print("M2的process_response.")
return response
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
访问http://127.0.0.1:8000/index/
, 根据实验结果, 不难看出:
1> 请求压根就没有到路由层!! 在请求经过中间件时,M1中间件就返回了一个结果给浏览器了!!
2> M1中间件拦截请求后, M1后面的中间件M2的请求和响应方法都不会执行!!
即如果在process_request方法中,返回了结果,后面的中间件就不走了,但同级别的process_response还是要走!!
# 源码分析
随便找一个Django写好的中间件,查看源码
比如'django.middleware.security.SecurityMiddleware',点击进去.
SecurityMiddleware类继承了MiddlewareMixin,SecurityMiddleware类中有process_request、process_response方法.
如何执行这两个方法的呢?再看父类MiddlewareMixin.
class MiddlewareMixin:
... ...
def __call__(self, request): # no,别看见这个__call__就以为是元类.Hhh.
if asyncio.iscoroutinefunction(self.get_response):
return self.__acall__(request)
response = None
# -- 主要看以下6行代码
# 在`最原始的方式`中,print("来了")、print("走了")这两地方的代码源码中使用反射来替换了!!!
# print("来了")
if hasattr(self, 'process_request'):
response = self.process_request(request)
# 执行视图函数中的代码
# ★ 若process_request函数有返回值,就不会执行视图函数中的代码
response = response or self.get_response(request)
# print("走了")
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
# ★ 若有process_response函数,最终<返回浏览器>的是process_response的返回值
return response
-- So,process_request和process_response方法的返回值都是可以有特殊作用的!!!
那么为啥会按照顺序执行中间件的request,再逆向response呢??很好奇.简单查阅了下资料.
参考文档链接:https://www.bbsmax.com/A/RnJW3pMyJq/
wsgi.py --> get_wsgi_application --> WSGIHandler --> WSGIHandler --> load_middleware
class BaseHandler:
_view_middleware = None
_template_response_middleware = None
_exception_middleware = None
_middleware_chain = None
def load_middleware(self, is_async=False):
self._view_middleware = []
self._template_response_middleware = []
self._exception_middleware = []
# 简单理解就是 handler = convert_exception_to_response(self._get_response)
# 而特别注意的是convert_exception_to_response,它是一个装饰器!!
# 这就涉及到装饰器的本质啦.比如装饰test函数,@auth语法糖的本质就是auth(test)
# 简单来说,这里,当get_response()调用时,就会触发self._get_response()的执行!!
"""
def convert_exception_to_response(get_response):
@wraps(get_response)
def inner(request):
try:
response = get_response(request)
except Exception as exc:
response = response_for_exception(request, exc)
return response
return inner
"""
get_response = self._get_response_async if is_async else self._get_response
handler = convert_exception_to_response(get_response)
# 注:倒序的遍历配置文件中的中间件
for middleware_path in reversed(settings.MIDDLEWARE):
# 将中间件对应的类拿到
middleware = import_string(middleware_path)
try:
# 不用管,有些异步的处理.
# 简单点,就是mw_instance = middleware(handler),将中间件类进行了实例化.
# 类实例化会调用__init__函数.查看 我们自定义类继承的MiddlewareMixin的源码
# 它将handler,简单点说就是将_get_response这个函数作为了类实例的get_response属性的值!
"""
class MiddlewareMixin:
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
"""
adapted_handler = self.adapt_method_mode(
middleware_is_async, handler, handler_is_async,
debug=settings.DEBUG, name='middleware %s' % middleware_path,
)
mw_instance = middleware(adapted_handler)
# 接下来的是三个反射,别管异步,就是看中间件类有没有这三个方法,有的话,加入到一开始定义的三个列表中
# 注意,中间件是倒序遍历的,这里添加到列表中的顺序,有的是从头添加有的是从末尾添加.
if hasattr(mw_instance, 'process_view'):
self._view_middleware.insert(
0,
self.adapt_method_mode(is_async, mw_instance.process_view),
)
if hasattr(mw_instance, 'process_template_response'):
self._template_response_middleware.append(
self.adapt_method_mode(is_async, mw_instance.process_template_response),
)
if hasattr(mw_instance, 'process_exception'):
# The exception-handling stack is still always synchronous for
# now, so adapt that way.
self._exception_middleware.append(
self.adapt_method_mode(False, mw_instance.process_exception),
)
# ★ 这行代码特别重要..
# convert_exception_to_response这个装饰器又出现了
# 只不过这次,它装饰的是mw_instance,也就是中间件类的实例
handler = convert_exception_to_response(mw_instance)
"""
我们举个例子,来脑补整个for循环的过程.假设有Md1、Md2、Md3这个三个中间件.
经过上面的分析,我们知道循环体里,会对每个中间件类Md1、Md2、Md3进行实例化.实例化会调用各自的__init__方法
因为是倒序遍历的.所以一顿捯饬.Md1、Md2、Md3这个三个类的类实例化对象的get_response属性的值分别是:
Md3().get_response = _get_response
Md2().get_response = Md3()
Md1().get_response = Md2()
"""
# 遍历完成后,self._middleware_chain的值是Md1()
self._middleware_chain = handler
当self._middleware_chain这个方法调用时,即Md1()(),触发Md1这个类里的__call__方法
Ps:_middleware_chain这个方法的调用在BaseHandler类的get_response方法中
get_response在WSGIHandler(base.BaseHandler)这个类的__call__方法中进行了调用!!
def __call__(self, request): # 注,别看见这个__call__就以为是元类.Hhh.
response = None
if hasattr(self, 'process_request'):
response = self.process_request(request)
response = response or self.get_response(request)
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
return response
"""
还是刚才那个栗子,我们有Md1、Md2、Md3这三个中间件.
Md1()(),触发Md1这个类里的__call__方法.
ok,先执行Md1()的process_request方法,然后执行Md1().get_response
重点来了,Md1().get_response里存储的是Md2()
Md1().get_response()相当于Md2()(),会触发Md2这个类里的__call__方法.
同理,执行这个call方法时,会先执行Md2()的process_request方法
接着,Md2().get_response()相当于Md3()(),又开始触发Md3这个类里的__call__方法.
同理,执行这个call方法时,会先执行Md3()的process_request方法
接着,Md3().get_response()相当于执行_get_response这个函数,该函数会进行路由匹配执行视图函数等.
然后,依次执行Md3的process_response、Md2的process_response、Md1的process_response
"""
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# 中间件总结
注册了三个中间件md1、md2、md3,每个中间件都有process_request、process_view、process_response函数.
正常情况情况下,process_request和process_view的返回值为None.process_response的返回值为response.
1> 图中,红色箭头为正常的流程!!
依次执行完process_request后,执行路由匹配;
路由匹配完成后,会再次回到中间件md1的位置.依次执行每个中间件的process_view;
接着执行路由对应的视图函数.. 视图函数return render(),本质return的是一堆字符串,会被Django封装到response中.
不管怎样,视图函数肯定会有返回.不管是 render、HttpResponse、还是 JsonResponse.
接着将Django封装好的response传递给process_response函数的response参数!!从后到前依次执行process_response.
2> 仅当中间件md2的process_request有返回值return HttpResponse("Hello World!"),绿色箭头为执行流程.
3> 仅当中间件md2的process_view有返回值return HttpResponse("Hello World!"),紫色箭头为执行流程.
★★★ 敲黑板!
我实验过了,随便访问个url.eg:127.0.0.1/xxx/会报错,Not Found: /xxx/
process_request、process_response会执行,process_request不会!
也就是说!路由匹配成功/项目中配置了这个路由,才会接着执行process_view!!
还有两个函数,了解下即可,基本上不会用.
def process_exception(self,request,exception):
# 会捕获到视图函数中的异常,exception就是视图函数中具体的异常.
# 可以记录日志,返回浏览器一个自定义的异常页面等.
print("异常了", exception)
return HttpResponse("异常了")
def process_template_response(self,request,response):
# 几乎不会用
pass
"""
★ 应用场景
"""
情景一:
当问登陆界面,登陆成功,会返回登陆凭证(一串随机字符串),会保存到浏览器中.
下一次,浏览器携带着这个登陆凭证访问网址的其他页面,在中间件的process_request中进行用户校验,校验通过了,才能成功访问.
情景二:
访问的是http://www.xxxx.com,该网站通过ajax向api.douyin.com这个接口拿数据
数据虽然能返回,但到达浏览器的时候,因为跨域名请求,返回的数据会被浏览器的同源策略拦截.
怎么让浏览器不拦截呢?api.douyin.com接口所在服务器的中间件的process_response中加个响应头!
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
Q1: 如何验证,process_request在执行时,没有执行路由匹配?
A1: 通过打印 request.resolver_match
Q2: 如何验证, 视图函数返回的内容被Django封装到了response中?
A2: 通过打印 response.__dict__
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class Mymd1(MiddlewareMixin):
def process_request(self, request):
# print(request.__dict__)
print(hasattr(request, "resolver_match"), request.resolver_match)
print("M1的p_req")
def process_view(self, request, view_func, view_args, view_kwargs):
# request:请求相关数据;view_func:路由匹配成功后的视图函数;view_args、view_kwargs:动态路由时传递的参数;
print(hasattr(request, "resolver_match"), request.resolver_match)
print(request, view_func)
print("M1的p_v")
def process_response(self, request, response):
print("M1的p_res")
return response
class Mymd2(MiddlewareMixin):
def process_request(self, request):
print("M2的p_req")
# return HttpResponse("Hello World!")
def process_view(self, request, view_func, view_args, view_kwargs):
print("M2的p_v")
# return HttpResponse("Hello World!")
def process_response(self, request, response):
print("M2的p_res")
return response
class Mymd3(MiddlewareMixin):
def process_request(self, request):
print("M3的p_req")
def process_view(self, request, view_func, view_args, view_kwargs):
print("M3的p_v")
def process_response(self, request, response):
print(response.__dict__)
print("M3的p_res")
return response
"""
True None
M1的p_req
M2的p_req
M3的p_req
True ResolverMatch(func=apps.api.views.index, args=(), kwargs={}, url_name=None, app_names=['api'], namespaces=['x1'], route=api/index/)
<WSGIRequest: GET '/api/index/'> <function index at 0x7f9cf67b3940>
M1的p_v
M2的p_v
M3的p_v
函数
M3的p_res
M2的p_res
{'headers': {'Content-Type': 'text/html; charset=utf-8'}, '_charset': None, '_resource_closers': [], '_handler_class': None, 'cookies': <SimpleCookie: >, 'closed': False, '_reason_phrase': None, '_container': [b'\n\n<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>\xe9\xa6\x96\xe9\xa1\xb5</title>\n</head>\n<body>\n\n \n <li>ALEX_Aqwe</li>\n\n \n <li>ALEX_Bqwe</li>\n\n \n <li>ALEX_Cqwe</li>\n\n\n\n\n<p>('hello', 'bb', 'ccc')</p>\n\n<p><h1>dc -- 20</h1></p>\n</body>\n</html>']}
M1的p_res
"""
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# csrf跨站请求
为了保证数据提交的安全性!
看这篇博文, 写的很好:https://www.jianshu.com/p/2249726c1391
针对, Django的csrf中间件具体如何实现的, 查看博文:https://blog.csdn.net/Deft_MKJing/article/details/90348835
CSRF(Cross Site Request Forgery)跨站点伪造请求,举例来讲:
某个恶意的网站上有一个指向你的网站的链接,如果某个用户已经登录到你的网站上了,那么当这个用户点击这个恶意网站上的那个链接时
就会向你的网站发来一个请求,你的网站会以为这个请求是用户自己发来的,其实呢,这个请求是那个恶意网站伪造的
为了避免上面情况的出现,Django引用了CSRF防护机制;
Django第一次响应来自某个客户端的请求时,会在服务器端随机生成一个 token,并把这个 token 放在 cookie 里.
然后后续每次 POST 请求都会带上这个 token,这样就能避免被 CSRF 攻击. 如果POST请求中没有token随机字符串,则返回403拒绝服务.
具体一点来说:(一定程度的保证得先访问本网站的页面)
1> get请求,获取页面
在csrf中间件的process_response中将token放到cookies里. "HTTP_COOKIE":"sid=xxxx;csrftoken=xxxxx"
(在《响应头》里可以看到,服务器叫浏览器,“给爷设置个名为csrftoken的cookie”)
Set-Cookie: csrftoken=tT8xf5JIrcScIVvzJp4VfGbOzPZc2MmQn7lGx0BdjX8qv84rbz2URzE1dfINU53n;
注意,GET请求的页面里得有{{csrf_token}},才会在cookie里生成csrftoken的值!!!
2> post请求,提交csrf_token+其它表单数据.
在csrf中间件的process_request中先获取cookie,在process_view中进行验证比对.
未提交csrf_token或者提交的csrf_token跟cookies里的不一样就失败(比较之前,会先进行反向解析清洗).压根不会到视图层.
很长一段时间里,我有这样一个疑惑.
在表单里写的{% csrf_token%}生成的csrfmiddlewaretoken 和 在cookies里的csrftoken 的值是不一样的..
而且每次get请求刷新页面, csrfmiddlewaretoken的值会变, csrftoken的值是不会变.. 那Django的csrf中间件是如何进行的403验证的?? 今儿看到了一遍文章 里面是这样写的:
`这个时候就进入最核心的CSRF防御的Token验证过程了,首先会去Cookies中拿到csrf_token,然后如果是表单,直接拿出csrfmiddlewaretoken; 如果是Ajax,就从header里面拿X-CSRF-Token; 根据上面的Token算法,这不是比较这两个token,因为肯定不相同,我们需要反向解析清洗出真正的csrf_secret值,对比如果相同,就是正常访问,如果不同,依然是熟悉的403页面,禁止访问`
PS,若是csrf跨站请求伪造,因为没有访问当前页面,所以拿不到csrfmiddlewaretoken或X-CSRF-Token.比较肯定失败!
★ 关键源码:(通过这几行源码,可以找到解决403错误的方法!!)
"""
def process_view(self, request, callback, callback_args, callback_kwargs):
request_csrf_token = ""
if request.method == "POST":
try:
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
except OSError:
pass
if request_csrf_token == "":
# CSRF_HEADER_NAME 默认: 'HTTP_X_CSRFTOKEN' --> 用于 CSRF 认证的请求头的名称
# 与 request.META 中的其他 HTTP 头文件一样, 从服务器接收到的头文件名通过将所有字符转换为大写字母
# 用下划线代替任何连字符, 并在名称中添加 'HTTP_' 前缀进行规范化.
# 例如, 如果你的客户端发送了一个 'X-XSRF-TOKEN' 头, 配置应该是 'HTTP_X_XSRF_TOKEN'.
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
"""
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
表单的csrf验证: `{% csrf_token %}`
ajax的post请求的csrf验证: `data: {'csrfmiddlewaretoken': '{{ csrf_token }}'}`
2
home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<title>Title</title>
</head>
<body>
<div style="margin: 20px">
<form action="" method="post">
<!--
会生成一个隐藏的input标签,key为csrfmiddlewaretoken,value为Django给我们随机生成的
-->
{% csrf_token %}
<p>username: <input type="text" name=""></p>
<p>password: <input type="password" name=""></p>
<input type="submit" name="" id="">
</form>
<br>
<button class="btn">按钮</button>
</div>
<script>
$('.btn').click(function () {
$.ajax({
url: '',
type: 'post',
/* -- 第一种方式
data: {
'username': 'egon',
// k值必须为csrfmiddlewaretoken!!
'csrfmiddlewaretoken': $('[name="csrfmiddlewaretoken"]'.val()),
},
*/
// 第二种方式
data: {'username': 'egon', 'csrfmiddlewaretoken': '{{ csrf_token }}'},
success: function () {}
})
})
</script>
</body>
</html>
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
# csrf验证的装饰器
适用场景: 绝大多数方法都要验证,极个别不验证; 绝大多数都不验证, 极个别要验证.
FBV的csrf装饰器
# -- 需要验证加csrf_protect;不需要验证加csrf_exempt
from django.views.decorators.csrf import csrf_protect, csrf_exempt
# -- csrf中间件不注释,func方法加上@csrf_exempt装饰器就不需要验证啦!
# -- csrf中间件注释掉,func方法加上@csrf_protect装饰器就需要验证啦!
# @csrf_protect
# @csrf_exempt
def func(request):
return render(request, 'func.html')
2
3
4
5
6
7
8
9
10
CBV的csrf装饰器
回顾一下: CBV添加装饰器有三种方式.
1> csrf中间件注释掉,CBV验证csrf的装饰器csrf_protect使用三种方式中任何一种添加都会生效!!
2> csrf中间件不注释掉.CBV不验证csrf的装饰器csrf_exempt.只有使用第三种方式才会生效!!
from django.views.decorators.csrf import csrf_protect, csrf_exempt
from django.utils.decorators import method_decorator # -- CBV添加装饰器
from django.shortcuts import HttpResponse
# @method_decorator(csrf_protect, name='post') # -- ok!
# @method_decorator(csrf_exempt, name='post') # -- no!
class Foo(View):
# @method_decorator(csrf_protect) # -- ok! 注意,此处该视图类里的方法都会进行csrf验证.
# @method_decorator(csrf_exempt) # -- ok! 注意,此处该视图类里的方法都不会进行csrf验证.
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, request):
return HttpResponse('foo_get')
# @method_decorator(csrf_protect) # -- ok!
# @method_decorator(csrf_exempt) # -- no!
def post(self, request):
return HttpResponse('foo_post')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19