Django-13:django中间件
django中间件
下图为django请求生命周期图:
![image-20220901180651056](https://s2.loli.net/2023/02/18/HGtxJANaqUIzkgX.png)
可以看到,请求在经过web网关接口(wsgiref模块)后,下一道“关卡”就是中间件,在django中默认有7个中间件,并且支持自定义中间件。
请求来的时候,会依次通过这7个自带的以及自定义的中间件校验,随后才会经过路由层、视图层等等…
请求走的时候,仍然会再依次校验,最后通过网关接口返回给用户。
查看django默认的中间件
-
settings.py文件
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
在django的设置文件中,大量使用了将模块路径以字符串的方式导入,在后面的章节中,会介绍如何实现。
总之默认的有7个中间件,后面我们要研究的是如何自定义中间件。
一、自定义中间件
研究django中间件代码规律
-
SessionMiddleware
class SessionMiddleware(MiddlewareMixin): def process_request(self, request): '''略''' def process_response(self, request, response): return response
-
CsrfViewMiddleware
class CsrfViewMiddleware(MiddlewareMixin): def process_request(self, request): '''略''' def process_view(self, request, callback, callback_args, callback_kwargs): return self._accept(request) def process_response(self, request, response): return response
-
AuthenticationMiddleware
class AuthenticationMiddleware(MiddlewareMixin): def process_request(self, request): '''略'''
发现:
- 定义中间件类中,类的方法名都很相似,如:process_request、process_response、process_view等。
- 都继承了MiddlewareMixin类。
django中,支持自定义中间件,并且暴露给我们五个可以自定义的方法:
-
process_request
-
process_response
-
process_view
-
process_template_response
-
process_exception
其中,process_request和process_response为必须掌握,剩余三个了解即可。
所以,我们自定义的中间件,也需要拥有这些方法,才可以对请求和响应的时候进行校验。
1.1 步骤
自定义中间件步骤:
-
一、在项目名或者应用名下创建一个任意名称的文件夹,并在该文件夹内创建任意py文件。
尽量是在应用下创建,这样在注册的时候可以补全。
-
二、在该Py文件内需要书写类,其中该类必须继承MiddlewareMixin
''' 因为需要继承MiddlewareMixin类,所以需要先导入 ''' from django.utils.deprecation import MiddlewareMixin
随后定义类,以及类中的五个方法(用到几个就写几个)
class MyMiddleware1(MiddlewareMixin): def process_request(self, request): '''略''' def process_response(self, request, response): '''略'''
本小结只对大体过程进行介绍,中间件类的更多详情,见1.2章节及以后。
-
三、需要将类的路径,以字符串的形式,注册到settings配置文件中
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 自定义中间件 'app_01.testmiddleware.testmiddleware.MyMiddleware1' ]
注:在注册的时候,一定要具体到类,而不是模块,否则就会报错:
django.core.exceptions.ImproperlyConfigured: WSGI application ‘djangoProject.wsgi.application’ could not be loaded;
1.2 process_request
在1.1章节中提到了django自定义中间件给我们提供的5个方法,本小结主要介绍process_request
结论:
-
请求来的时候,需要经过每一个中间件里面的process_request方法。
结果的顺序是按照配置文件中注册的中间件从上往下的顺序依次执行.
-
如果中间件里面没有定义该方法,那么直接跳过执行下一个中间件.
-
如果该方法返回了HttpResponse对象,那么请求将不再继续往后执行,而是直接原路返回,如:返回校验失败不允许访问…等等。
process_request方法就是用来做全局相关的所有限制功能
验证:
一、验证是否请求来的时候,都会经过每个中间件里的process_request方法
# 中间件
from django.utils.deprecation import MiddlewareMixin
class M1(MiddlewareMixin):
def process_request(self, request):
print('M1')
class M2(MiddlewareMixin):
def process_request(self, request):
print('M2')
class M3(MiddlewareMixin):
def process_request(self, request):
print('M3')
class M4(MiddlewareMixin):
def process_request(self, request):
print('M4')
# settings文件
MIDDLEWARE = [
'.....略......',
'mid.middleware.M1',
'mid.middleware.M2',
'mid.middleware.M3',
'mid.middleware.M4',
]
# 与1.1章节中不一致,测试时请勿直接复制粘贴。
![image-20221204225801876](https://s2.loli.net/2023/02/18/j7mSf8JZekE4KXw.png)
此时访问任意页面,即可看到,每一个自定义中间件(类)中的process_request方法都执行了。
注:执行顺序,是按照settings文件内注册的顺序,依次来执行的。
二、如果中间件内没有定义process_request方法,那么直接跳过,执行下一中间件
-
settings
MIDDLEWARE = [ '''略''' 'mid.middleware.M4', 'mid.middleware.M1', 'mid.middleware.M2', 'mid.middleware.M3', ]
-
自定义中间件
from django.utils.deprecation import MiddlewareMixin class M1(MiddlewareMixin): def process_request(self, request): print('M1') class M2(MiddlewareMixin): pass class M3(MiddlewareMixin): pass class M4(MiddlewareMixin): def process_request(self, request): print('M4')
-
访问任意页面查看效果
由于在注册的时候是先注册的M4这个中间件,所以自然先执行的也是M4类内的process_request方法。
M2和M3类中没有process_request方法,所以直接跳过。
三、如果该方法返回了HttpResponse对象,那么请求将不再继续往后执行,而是原路返回
-
settings
MIDDLEWARE = [ '''略''' 'mid.middleware.M4', 'mid.middleware.M1', 'mid.middleware.M2', 'mid.middleware.M3', ]
-
中间件
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse class M1(MiddlewareMixin): def process_request(self, request): print('M1') class M2(MiddlewareMixin): def process_request(self, request): print('M2') return HttpResponse('hello world') class M3(MiddlewareMixin): def process_request(self, request): print('M3') class M4(MiddlewareMixin): def process_request(self, request): print('M4')
-
查看效果
可以看到,同样是先注册了M4,所以正常执行process_request方法,随后依次按照注册顺序执行M1和M2,但是M2内部直接返回了一个HttpResponse对象,M3就直接跳过了。
由于请求原路返回,所以并不会再通过路由层、视图层等等,直接返回给客户端一个hello world,而不是原本的login页面。
1.3 process_response
总结:
- 响应走的时候,需要经过每一个中间件里面的process_response方法,该方法必须有两个额外的参数request、response。
- 该方法必须返回一个HttpResponse对象,默认返回的就是形参response,也可以返回自己的。
- 顺序是按照配置文件中注册了的中间件从下往上依次经过,如果没有定义process_response方法,就直接跳过执行下一个。
- 当process_request方法中直接返回了HttpResponse对象,由于直接返回,所以执行同级别及以上中间件的process_response方法,后注册的将不在经过
验证:
一、process_response执行顺序是否是自下而上
-
settings
MIDDLEWARE = [ 'mid.middleware.M1', 'mid.middleware.M2', 'mid.middleware.M3', 'mid.middleware.M4', ]
-
中间件
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse class M1(MiddlewareMixin): def process_request(self, request): print('M1_request') def process_response(self, request, response): print('M1_response') return response class M2(MiddlewareMixin): def process_request(self, request): print('M2_request') def process_response(self, request, response): print('M2_response') return response class M3(MiddlewareMixin): def process_request(self, request): print('M3_request') def process_response(self, request, response): print('M3_response') return response class M4(MiddlewareMixin): def process_request(self, request): print('M4_request') def process_response(self, request, response): print('M4_response') return response
-
查看效果:
二、当process_request方法就已经返回了HttpResponse对象,那么响应走的时候process_response怎么执行
-
settings略
-
中间件
from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse class M1(MiddlewareMixin): def process_request(self, request): print('M1_request') def process_response(self, request, response): print('M1_response') return response class M2(MiddlewareMixin): def process_request(self, request): print('M2_request') return HttpResponse('111') def process_response(self, request, response): print('M2_response') return response class M3(MiddlewareMixin): def process_request(self, request): print('M3_request') def process_response(self, request, response): print('M3_response') return response
-
查看效果
可以看到,M2的process_request方法直接返回了HttpResponse对象后,直接执行了同级别的process_response方法,并且按照注册顺序,执行在前面的所有中间件process_response方法。
拓展:flask框架也有一个中间件,但是它只要返回数据了,就必须经过所有中间件里面的类似于django中的process_reponse方法。
1.3 其他
了解即可
process_view
- 路由匹配成功之后,执行视图函数之前,会自动执行中间件里面的该方法。
- 顺序是按照配置文件中注册的中间件从上往下的顺序依次执行
process_template_response
- 返回的HttpResponse对象有render属性的时候才会触发
- 顺序是按照配置文件中注册了的中间件从下往上依次经过
process_exception
- 当视图函数中出现异常的情况下触发,顺序是按照配置文件中注册了的中间件从下往上依次经过。
二、csrf跨站请求伪造
在前面的文章中,会有提到在发送post请求时,需要在settings文件中注释掉一个行代码,通过一章节呢也知道了注释掉的是自带中间件的其中之一。
本章节简单介绍这个中间件的作用,以及如何不注释的情况下,可以正常提交post请求。
案例:钓鱼网站
- 搭建一个校园食堂饭卡充值网站web1,以及一个高仿的web2。
- 学生A登陆官方的web1网站,输入个人信息后余额充值成功。
- 当学生A通过钓鱼网站web2进行充值时,不管表单内容怎么填写,最后钱都充值到了学生B的账户上。
代码:
官方充值渠道
''' 路由略、设置文件注释csrf中间件、模板文件引入jQuery和BootStrap。 ''' def csrf(request): if request.method == 'POST': target_name = request.POST.get('target_name') money = request.POST.get('money') print(f'{target_name}的账户上,充值了{money}¥') return HttpResponse(f'{target_name}的账户上,充值了{money}¥') return render(request,'csrf.html')
<div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <h1 class="text-center">饭卡充值系统(官方)</h1> <br> <form class="form-horizontal" action="" method="post"> <div class="form-group"> <label for="ipt_uname" class="col-sm-2 control-label">充值账户</label> <div class="col-sm-10"> <input type="text" class="form-control" id="ipt_uname" placeholder="张三" name="target_name"> </div> </div> <div class="form-group"> <label for="ipt_money" class="col-sm-2 control-label">充值金额</label> <div class="col-sm-10"> <input type="text" class="form-control" id="ipt_money" placeholder="100" name="money" > </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-success">充值</button> </div> </div> </form> </div> </div> </div>
高仿钓鱼网站
def csrf(request): return render(request,'csrf.html')
<div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <h1 class="text-center">饭卡充值系统(钓鱼)</h1> <br> <form class="form-horizontal" action="http://127.0.0.1:8000/csrf" method="post" > <div class="form-group"> <label for="ipt_uname" class="col-sm-2 control-label">充值账户</label> <div class="col-sm-10"> <input type="text" class="form-control" id="ipt_uname" placeholder="张三"> <!--隐藏标签,表单提交后,提交的target_name为李四,而不是上面用户填写的。--> <input type="text" name="target_name" value="李四" style="display: none"> </div> </div> <div class="form-group"> <label for="inputPassword3" class="col-sm-2 control-label">充值金额</label> <div class="col-sm-10"> <input type="text" class="form-control" id="inputPassword3" placeholder="100" name="money" > </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-success">充值</button> </div> </div> </form> </div> </div> </div>
可以看到,由于钓鱼网站form表单的属性action=“http://127.0.0.1:8000/csrf”,所以当受害者在钓鱼网站上进行“充值”时,不管需要充值的账户填什么,最终打款到账的都是隐藏标签的name属性值(李四)
如何规避上述问题?
-
csrf跨站请求伪造校验
网站在给用户返回一个具有提交数据功能页面的时候,会给这个页面加一个唯一标识,当这个页面朝后端发送post请求的时候,后端会先校验唯一标识,如果唯一标识不对直接拒绝(403 forbbiden)如果成功则正常执行。
详情见2.1
2.1 csrf校验
form表单如何符合校验:
-
在HTML代码中添加一行代码{% csrf_token %},如:
<form action="" method="post"> {% csrf_token %} <p>username:<input type="text" name="username"></p> <p>target_user:<input type="text" name="target_user"></p> <p>money:<input type="text" name="money"></p> <input type="submit"> </form>
{% csrf_token %}会动态的渲染出一个input标签,如下:
当页面提交时,会把这个唯一标识发送给后端,django后端封装到request.POST中,当确定该页面是由后端自己发送出去的时候,则正常提交,否则就403。
由于页面在提交时,会把input表单的数据都进行提交,所以{% csrf_token %}放在哪里都可以,最终都能传给django后端。
ajax如何符合校验:
-
第一种:利用标签查找来获取页面上的随机字符串
//获取type属性值为submit的标签,并绑定点击事件。 $('[type=submit]').on('click',function () { $.ajax({ type:'POST', url:'', data:{ //获取{% csrf_token %}动态生成表单的数据,以及在2.0章节中其他字段(表单)的数据。 'csrfmiddlewaretoken':$('[name=csrfmiddlewaretoken]').val(), 'target_name':$('#ipt_uname').val(), 'money':$('#ipt_money').val(), }, success:function () { } }) })
利用标签查找的方式,手动的获取动态生成表单的name属性值,以及value属性的值,随后利用ajax发上给后端做csrf校验。
-
第二种:利用模板语法提供的快捷书写
'''本质上还是第一种,只不过节省了一些代码''' $('[type=submit]').on('click',function () { $.ajax({ type:'POST', url:'', data:{ // 这里注意引号 'csrfmiddlewaretoken':'{{ csrf_token }}', 'target_name':$('#ipt_uname').val(), 'money':$('#ipt_money').val(), }, success:function () { } }) })
因为使用到了django提供的模板语法,所以前后端分离的项目不适用,而且也并没有带来多大的帮助,所以推荐使用第三种方式。
-
第三种:从官方文档上,拷贝js代码并引入到自己的html页面
django文档链接:https://docs.djangoproject.com/en/1.11/ref/csrf/
复制下列代码
function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken'); function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function (xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } });
注意:
- html代码的执行顺序是从上到下的,浏览器由上至下解析html代码,如果在head里面引入js,可能会导致js执行时,页面标签还未加载,导致js找不到作用对象,从而失效。
- 所以在引入时,需要在body标签内的末尾引入。
2.2 csrf相关装饰器
应用场景:
- 1.网站整体都不校验csrf,就单单几个视图函数需要校验。
- 2.网站整体都校验csrf,就单单几个视图函数不校验。
装饰器方法名:
-
csrf_protect 需要校验
-
csrf_exempt 忽视校验
from django.views.decorators.csrf import csrf_protect,csrf_exempt
使用
FBV的场景下
-
直接装饰视图函数即可
@csrf_protect # 需要校验 def csrf(request): if request.method == 'POST': target_name = request.POST.get('target_name') money = request.POST.get('money') print(f'{target_name}的账户上,充值了{money}¥') return HttpResponse(f'{target_name}的账户上,充值了{money}¥') return render(request,'csrf.html')
CBV的场景下
-
由于CBV的添加装饰器方式有三种,所以我们需要依次对这三种情况进行测试,测试两个装饰器方法是否可以实现效果。
第一种,在类中给具体方法进行装饰
from django.views.decorators.csrf import csrf_protect,csrf_exempt from django.utils.decorators import method_decorator from django.views import View class MyCsrfToken(View): def get(self,request): return HttpResponse('get') # csrf_protect,第一种方式可以。 @method_decorator(csrf_protect) def post(self,request): return HttpResponse('post') # csrf_exempt,第一种方式不可以。 ''' @method_decorator(csrf_exempt) def post(self,request): return HttpResponse('post') '''
篇幅问题,直接得出结论:第一种方式,csrf_protect可以发挥需要校验的功能,但是csrf_exempt为生效,仍需要校验
第二种,给类进行装饰,形参指定到具体类中方法
from django.views.decorators.csrf import csrf_protect,csrf_exempt from django.utils.decorators import method_decorator from django.views import View @method_decorator(csrf_protect,name='post') # 针对csrf_protect,第二种方式可以。 class MyCsrfToken(View): def get(self,request): return HttpResponse('get') def post(self,request): return HttpResponse('post') @method_decorator(csrf_exempt,name='post') # 针对csrf_exempt,第二种方式不可以。 class MyCsrfToken(View): def get(self,request): return HttpResponse('get') def post(self,request): return HttpResponse('post')
与第一种方式一样。
第三种,给父级方法添加装饰器
from django.views.decorators.csrf import csrf_protect,csrf_exempt from django.utils.decorators import method_decorator from django.views import View # @method_decorator(csrf_exempt,name='dispatch') # 第三种方式中又分为两种,一种是在上面指名道姓,一种是在下面 class MyCsrfToken(View): # @method_decorator(csrf_protect) # 针对csrf_protect 第三种方式可以 # @method_decorator(csrf_exempt) # 针对csrf_exempt 第三种方式可以 def dispatch(self, request, *args, **kwargs): return super(MyCsrfToken, self).dispatch(request,*args,**kwargs) def get(self,request): return HttpResponse('get') def post(self,request): return HttpResponse('post')
总结:
- CBV的模式下,如果想添加CSRF相关的装饰器,那么csrf_exempt 跳过校验就必须采用第三种方式,也就是给父类中的dispath方法添加装饰器。
三、importlib模块
django中间件的开启与关闭,直接在settings中进行注释就可以完成,这种热插拔式是怎么实现的呢?
补充模块(知识点):
-
importlib
from middleware import mid print(mid) # 输出结果<module 'middleware.mid' from 'D:\\路径\\middleware\\mid.py'> ''' 使用importlib模块 ''' import importlib utils = 'middleware.mid' res = importlib.import_module(utils) # 相当于:from middleware import mid print(res) # 输出结果<module 'middleware.mid' from 'D:\\路径\\middleware\\mid.py'>
可以看到,mid和res都是mid.py这个对象,所以importlib模块,就是做这种将模块以字符串的形式导入,也就是为什么settings.py中都是以这种形式导入模块的。
但是,importlib只能到方法名
# 常规的导入可以直接导入py文件内的属性,如str1属性。
from middleware.mid import str1
print(str1)
# 但是使用了importlib的,就只能导入到py文件,需要使用到内部属性就直接.获取。
res = importlib.import_module('middleware.mid')
print(res.str1)