64、Django中间件

Django框架 / 2021-03-05

参考博客:博客园-JsonJi,原文链接:https://www.cnblogs.com/Dominic-Ji/p/9229509.html

一、Django中间件简述

官方的说法:中间件是一个用来处理Django的请求和响应的框架级别的钩子。它是一个轻量、低级别的插件系统,用于在全局范围内改变Django的输入和输出。每个中间件组件都负责做一些特定的功能。

但是由于其影响的是全局,所以需要谨慎使用,使用不当会影响性能。

说的直白一点中间件是帮助我们在视图函数执行之前和执行之后都可以做一些额外的操作,它本质上就是一个自定义类,类中定义了几个方法,Django框架会在请求的特定的时间去执行这些方法。

Django自带7个中间件,每个都有对应的功能,就类似于七道关卡,所以进出的数据都要经过这七道关卡的检查和处理!Django还支持程序员自定义中间件,并且暴露给程序员五个可以自定义中间件的方法!

用Django开发项目的时候,只要是设计到全局相关的功能都可以使用中间件方便地完成,比如:

  • 全局用户身份校验
  • 全局用户权限校验
  • 全局访问频率校验

为什么说Django中间件是Django的关卡?

  1. 请求来的时候需要先经过中间件才能到达真正的Django后端
  2. 响应走的时候最后也需要经过中间件才能发送出去

我们再来画一下Django请求生命周期图

Django请求生命周期图

二、自定义中间件

Django给我们提供了五个自定义中间件的方法,在使用这五个方法前,我们需要完成一些准备工作!

2.1、自定义中间件前戏

2.1.1、创建py文件
  1. 在项目下或应用下创建一个任意名称的文件夹。
  2. 在该文件夹内创建一个任意名称的py文件。
  3. 在该py文件内需要书写类,这个类必须继承MiddlewareMixin。
2.1.2、注册自定义中间件

经过上面的三个步骤后,就可以在这个类里面就可以自定义五个方法了,这个五个并不是全都要写,用几个才写几个。

  • process_request
  • process_response
  • process_view
  • process_template_response
  • process_exception

需要将类的路径以字符串的形式注册到配置文件中才能生效;在应用下创建写路径时会有提示,在别的地方创建则没有!

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',
    'blog.my_middleware.my_middleware.FirstMiddleWare',
    'blog.my_middleware.my_middleware.SecondMiddleWare'
]

2.2、必须会的两个方法

2.2.1、process_request
views代码
def register(request):
    return '我是视图函数的'
中间件代码
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse


class FirstMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('你是真的6')


class SecondMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        if request.method == 'GET':
            return HttpResponse('拒绝访问')
执行结果

终端打印了:你是真的6

页面上输出了:拒绝访问

总结
  1. 中间件的process_request方法是在执行视图函数之前执行!
  2. 当存在多个注册中间件时,按照配置文件注册顺序,从上到下执行process_request函数里面的代码;如该中间件无该方法,则跳过执行下一个。
  3. 当该方法返回一个Httpresponse对象时,用户请求将不再继续往后执行,而是直接原路将Httpresponse对象返回给用户。
  4. 不同中间件里的request参数都是同一个对象。

所以这个process_request方法多用来做全局相关的所有限制功能

2.2.2、process_response
中间件代码
class FirstMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('你是真的6')

    def process_response(self, request, response):
        print('我想改你的数据')
        return HttpResponse('我不会随便改你的数据')


class SecondMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('777')

    def process_response(self, request, response):
        print('数据准备被篡改')
        return HttpResponse('数据已被篡改!')
终端执行结果
你是真的6
777
数据准备被篡改
我想改你的数据

用户页面结果:我不会随便改你的数据

总结:

  1. 有两个额外形参:request、response;其中response就是后端返回给前端浏览器的内容,所以该方法必须返回一个HTTPresponse对象!
  2. 先执行各中间件的process_request接着执行视图函数最后才执行各中间件的process_response,各response顺序为注册顺序倒序执行,无则跳过!
  3. 默认返回的就是形参response,你也可以返回自己的东西!
2.2.3、二者结合

如在第一个process_request方法就已经返回了HttpResponse,那么响应走的时候是经过所有的中间件里面的process_response还是其它情况???

答案是不会再执行后面的中间件了,直接走同级别(同一个自定义中间件)的process_response返回,无则直接返回request中的response!

flask框架也有一个中间件,但是它的规律是只要返回数据了就必须经过所有中间件的process_response方法!

class FirstMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('你是真的6')
        return HttpResponse('我是真的666')

    def process_response(self, request, response):
        print('我想改你的数据')
        return HttpResponse('我不会随便改你的数据')


class SecondMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('777')

    def process_response(self, request, response):
        print('数据准备被篡改')
        return HttpResponse('数据已被篡改!')

2.3、了解三个方法

这三个方法用得较少,因此有所了解,知道有这个东西有什么用就行!

2.3.1、process_view

在路由匹配成功后执行视图函数前,会自动执行中间件里面的该方法,按照注册顺序执行。

该方法有四个参数

  • view_func是Django即将使用的视图函数。 (它是实际的函数对象,而不是函数的名称作为字符串。)
  • view_args是将传递给视图的位置参数的列表.
  • view_kwargs是将传递给视图的关键字参数的字典。 view_args和view_kwargs都不包含第一个视图参数(request)。
  • Django会在调用视图函数之前调用process_view方法。

它应该返回None或一个HttpResponse对象。 如果返回None,Django将继续处理这个请求,执行任何其他中间件的process_view方法,然后在执行相应的视图。 如果它返回一个HttpResponse对象,那么将不会执行Django的视图函数,而是直接在中间件中掉头,倒叙执行一个个process_response方法,最后返回给浏览器。

views代码
def register(request):
    print('我是视图函数的')
    return HttpResponse('来了来了')
中间件代码
class FirstMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('你是真的6')

    def process_response(self, request, response):
        print('我想改你的数据')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print('中间件1的process_view')


class SecondMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('777')

    def process_response(self, request, response):
        print('数据准备被篡改')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print('中间件2的process_view')
执行结果
你是真的6
777
中间件1的process_view
中间件2的process_view
我是视图函数的
数据准备被篡改
我想改你的数据
2.3.2、process_template_response

奇葩,参数跟response一样,也要返回response;视图函数返回的对象有一个render()才会触发该方法,执行顺序跟response一样。

2.3.3、process_exception

视图函数出现报错时触发,顺序也是从上到下

三、CSRF跨站请求伪造

为避免钓鱼网站,Django会给每一个返回的页面赋予一个随机的唯一标识符,在客户端提交POST请求时如果页面没有携带该标识会直接被拒绝访问403,匹配到则正常进行下一步。

这里有三种方法可以为页面设置CSRF标识符。

3.1、配置CSRF的三种方式

3.1.1、form表单

如果前端也是用的Django的模板语法,那么只需要在form表单里面写一个{{% csrf_token %}}即可!

<form action="#" method="post">
    {% csrf_token %}
    <p><label for="username">username</label>
</form>
3.1.2、Ajax
3.2.1、标签查找随机字符串
<script>
    $('#token').click(function(){
        $.ajax({
            url: '',
            type: 'POST',
            data: {'username':'sanxi', 'csrfmiddlewaretoken':$('[name=csrfmiddlewaretoken]').val()},
            success:function(){

            }
        })
    })
</script>
3.2.2、ajax模板语法

就是比上面短一点,还是有点麻烦。

<script>
    $('#token').click(function(){
        $.ajax({
            url: '',
            type: 'POST',
            data: {'username':'sanxi', 'csrfmiddlewaretoken':{{ csrf_token }}},
            success:function() {

            }
        })
    })
</script>
3.2.3、通用方式

上面的几种方法都是基于一个前提,即有模板语法的情况下能使用,实际开发环境前后端很可能是分离的,因此我们需要一种通用方式来适应任何环境!

新建一个js文件,可以放在static里面,拷贝以下代码,然后在HTML模板中引入该文件即可!

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);
    }
  }
});

比如这样

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    {% load static %}
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
    <script src="{% static 'js/csrf.js' %}"></script>  <!--像这样引入即可-->
    <script src="{% static 'js/jQuery v3.5.1.js' %}"></script>
    <script src="{% static 'js/bootstrap.min.js' %}"></script>
    <script src='https://127.0.0.1:8000/f/c7513d5d200a403fa510/?dl=1'></script>
</head>

3.2、CSRF装饰器

假设我们有这样的需求:

  1. 网站整体都不校验CSRF,就单单几个视图函数需要校验。
  2. 整体都校验,就几个不用校验。
    要导入两个装饰器
3.2.1、视图函数用装饰器

直接装就行了!

# @csrf_protect
@csrf_exempt
def register(request):
    return render(request, 'register.html')
3.2.2、CBV自定义类用装饰器
csrf_protect

需要校验CSRF,之前CBV三种引用装饰器方式都可以。

# @method_decorator(csrf_protect, name='post')  # 第二种指名道姓方式,可行。
class MyCsrfToken(View):
    # @method_decorator(csrf_protect)  # 第三种给dispatch加,可行。
    def dispatch(self, request, *args, **kwargs):
        return super(MyCsrfToken, self).dispatch(request, *args, **kwargs)
    
    @method_decorator(csrf_protect)  # 第一种方式,可行。
    def get(self, request):
        return HttpResponse('get')

    def post(self, request)
        return HttpResponse('post')
csrf_exempt

不需要校验CSRF,只能加给dispatch

class MyCsrfToken(View):
    @method_decorator(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')

四、程序实现拔插式设计

4.1、模块补充importlib

专门用来进行字符串形式的导入,有缺陷,最小单位只能到模块名。

import importlib
sanxi = 'blog.models'
import_result = importlib.import_module(sanxi)  # 等同于from blog import models

4.2、拔插式设计思想

这是基于Django中间件一个重要的编程思想,参考Djangosettings中间件功能拔插式设计!

让我们先建立一个包,里面放几个py文件,每个py定义个类和属性

class Cat(object):
    def __init__(self):
        ...  # 前期准备

    def talk(self, anima):
        print(f'动物{anima}在叫')

定义配置文件settings,把各py文件的路径和类名保存在列表中

ANIMAL_LIST = [
    'settings_common.cat.Cat',  # settings_common是我建的包,cat是文件名,Cat是类。
    'settings_common.dog.Dog',
    'settings_common.pig.Pig'
]

重头戏在包里面的init.py里面的代码

import settings
import importlib


def animal_talk(animal):  # 定义个统一的方法,在导入init时先执行获取名称空间
    for common_str in settings.ANIMAL_LIST:  # 取出配置文件中的字符串
        set_model, set_class = common_str.rsplit('.', 1)  # 解压赋值,set_model为模块名,set_class为模块里的类名
        model = importlib.import_module(set_model)  # 利用importlib将字符串转成导入模块
        class_name = getattr(model, set_class)  # 利用反射机制获取模块里面对应的方法
        class_obj = class_name()  # 统一生成类的对象
        class_obj.talk(animal)  # 利用鸭子类型直接调用talk方法

在启动用的py文件中,开始运行代码

import settings_common
settings_common.animal_talk('什么鬼')

此时神奇的事情发生了,代码一个不动,只要去settings中注释或者解开注释即可控制一个功能的开与关!!!

世间微尘里 独爱茶酒中