1. 中间件

中间件是 Django 用来处理请求和响应的钩子框架。

它是一个轻量级的、底层级的“插件”系统,用于全局性地控制Django的输入或输出,可以理解为内置的app或者小框架。

在django.core.handlers.base模块中定义了如何接入中间件,这也是学习Django源码的入口之一。

每个中间件组件负责实现一些特定的功能。例如,Django 包含一个中间件组件 AuthenticationMiddleware,它使用会话机制将用户与请求request关联起来。

中间件可以放在你的工程的任何地方,并以Python路径的方式进行访问。

可以把中间件理解成一个跟别的都没有依赖关系的独立的类,在某一特定的时刻,当条件满足就会被自动执行。

1.1. 自带中间件

Django 具有一些内置的中间件,并自动开启了其中的一部分,我们可以根据自己的需要进行调整。

1.2. 如何启用中间件

若要启用中间件组件,请将其添加到 Django 配置文件settings.py的 MIDDLEWARE 配置项列表中。

在 MIDDLEWARE 中,中间件由字符串表示。这个字符串以圆点分隔,指向中间件工厂的类或函数名的完整 Python 路径。下面是使用 django-admin startproject命令创建工程后,默认的中间件配置:

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中可以不使用任何中间件,如果你愿意的话,MIDDLEWARE 配置项可以为空。

1.3. 中间件调用的顺序问题

中间件是一组相互独立的组件,相对其他模块,中间件的摆放顺序十分重要(可以理解成配置文件中出现的顺序), MIDDLEWARE 具有先后关系,因为有些中间件会依赖其他中间件。

例如: AuthenticationMiddleware 需要在会话中间件中存储的经过身份验证的用户信息, 因此它必须在 SessionMiddleware 后面运行 。

中间件具体的调用顺序,跟具体哪个调用阶段相关,后面会详细讲解。

1.4. 系统自带中间件

1.4.1. Cache

  • 缓存中间件
  • 如果启用了该中间件,Django会以CACHE_MIDDLEWARE_SECONDS 配置的参数进行全站级别的缓存。

1.4.2. Common

  • 通用中间件
  • 该中间件为我们提供了一些便利的功能:
    • 禁止DISALLOWED_USER_AGENTS中的用户代理访问服务器
    • 自动为URL添加斜杠后缀和www前缀功能。如果配置项 APPEND_SLASH 为True , 并且访问的URL 没有斜杠后缀,在URLconf中没有匹配成功,将自动添加斜杠,然后再次匹配,如果匹配成功, 就跳转到对应的url。 PREPEND_WWW 的功能类似。
    • 为非流式响应设置Content-Length头部信息。
    • 源代码位于django.middleware.common模块中, 很容易读懂和理解,建议尝试。

1.4.3. GZip

该中间件必须位于其它所有需要读写响应体内容的中间件之前。
  • 内容压缩中间件
  • 用于减小响应体积,降低带宽压力,提高传输速度。
  • 如果存在下面情况之一,将不会压缩响应内容:
    • 内容少于200 bytes
    • 已经设置了 Content-Encoding 头部属性
    • 请求的 Accept-Encoding 头部属性未包含 gzip.
  • 可以使用 gzip_page()装饰器,为视图单独开启GZip压缩服务。

1.4.4. Conditional GET

  • 有条件的GET访问中间件,很少使用。

1.4.5. Locale

  • 本地化中间件
  • 用于处理国际化和本地化,语言翻译。

1.4.6. Message

  • 消息中间件
  • 基于cookie或者会话的消息功能,比较常用。

1.4.7. Security

  • 安全中间件
  • django.middleware.security.SecurityMiddleware中间件为我们提供了一系列的网站安全保护功能。 主要包括下列所示,可以单独开启或关闭:
    • SECURE_BROWSER_XSS_FILTER
    • SECURE_CONTENT_TYPE_NOSNIFF
    • SECURE_HSTS_INCLUDE_SUBDOMAINS
    • SECURE_HSTS_PRELOAD
    • SECURE_HSTS_SECONDS
    • SECURE_REDIRECT_EXEMPT
    • SECURE_SSL_HOST
    • SECURE_SSL_REDIRECT

1.4.8. Session

  • 会话中间件,非常常用。

1.4.9. Site

  • 站点框架。
  • 这是一个很有用,但又被忽视的功能。
  • 它可以让你的Django具备多站点支持的功能。
  • 通过增加一个site属性,区分当前request请求访问的对应站点。
  • 无需多个IP或域名,无需开启多个服务器,只需要一个site属性,就能搞定多站点服务。

1.4.10. Authentication

  • 认证授权框架
  • Django最主要的中间件之一,提供用户认证服务。

1.4.11. CSRF protection

  • 提供CSRF防御机制的中间件
  • X-Frame-Options
  • 点击劫持防御中间件

1.5. 自定义中间件-传统方法

中间件的自定义现在有两种方法,估计新版本会把旧的调用方法逐渐舍弃。

传统的定义中间件方法,需要利用系统的五大钩子函数, 根据我们的需要和钩子函数的调用的时机,我们可以实现其中的任意一个或多个:

  • process_request(self,request)
    • 签名:process_request(request)
  • process_response(self, request, response)
    • 签名:process_response(request, response)
    • 两个参数,request和response
      • request是请求内容
      • response是视图函数返回的HttpResponse对象
    • 该方法的返回值必须是一个HttpResponse对象,不能是None。
    • 方法在视图函数执行完毕之后执行,并且按配置顺序的逆序执行
  • process_view(self, request, view_func, view_args, view_kwargs)
    • 签名:process_view(request, view_func, view_args, view_kwargs)
      • request: HttpRequest 对象。
      • view_func:真正的业务逻辑视图函数(不是函数的字符串名称)。
      • view_args:位置参数列表
      • view_kwargs:关键字参数字典
    • 在Django调用真正的业务视图之前被执行,并且以正序执行
    • 当process_request()正常执行完毕后,会进入urlconf路由阶段,并查找对应的视图, 在执行视图函数之前,会先执行这个函数
    • 必须返回None 或者一个 HttpResponse 对象
      • 如果返回的是None,Django将继续处理当前请求,执行其它的process_view中间件钩子, 最后执行对应的视图。
      • 如果返回的是一个 HttpResponse 对象,Django不会调用业务视图,而是执行响应中间件, 并返回结果
  • process_exception(self, request, exception)
    • 签名:process_exception(request, exception)
      • request:HttpRequest对象
      • exception:视图函数引发的具体异常对象
    • 当一个视图在执行过程中引发了异常,Django将自动调用中间件的 process_exception方法 process_exception要么返回一个 None ,要么返回一个 HttpResponse 对象
      • 如果返回的是HttpResponse对象 ,模板响应和响应中间件将被调用
      • 如果返回None, 否则进行正常的异常处理流程
  • process_template_response(self,request,response)
    • 签名:process_template_response(request, response)
      • request:HttpRequest 对象
      • response: TemplateResponse 对象
    • 法在业务视图执行完毕后调用。
    • 正常情况下一个视图执行完毕,会渲染一个模板,作为响应返回给用户。 使用这个钩子方法,你可以重新处理渲染模板的过程,添加你需要的业务逻辑。
    • 对于 process_template_response()方法,也是采用逆序的方式进行执行的。

1.6. 中间件的执行顺序

关于中间件的执行顺序,总的原则如下:

  1. 中间件总的执行顺序是按照settings.py中定义的顺序
  2. 有按顺序从前向后执行的,也有按顺序从后向前执行的。具体我们下面分别产生。
    • 顺序从前向后执行的:
      • process_request(self,request)
      • process_view(self, request, view_func, view_args, view_kwargs)
    • 顺序从后向前执行:
      • process_response(self, request, response)
      • process_exception(self, request, exception)
      • process_template_response(self,request,response)
  3. 任何中间件一旦返回Response,则从本层中间件开始调用返回response的操作,此操作从后向前执行, 没有被执行的中间件将不再执行。
  4. 一旦中间件产生异常,则直接从本层中间件中断执行,转向异常处理,其余没有执行的中间件将不再执行。

具体执行顺序请参看下面图示:

  • 简单中间件

    • 最简单的只有request和response的中间件, 请求从第一个中间件的request开始执行, 直到最后执行完视图后,从后向前依次返回。
    • 如果某个中间件返回Response,如图中中间件3, 则直接执行返回response的中间件,中间件 4,5,6将不再被执行,此时视图也不会被执行。

    中间件执行顺序

  • 带process_view的中间件

    • 此时从前向后执行完request后,则返回从第一个中间件开始执行view操作,然后才会 执行程序的视图模块,然后返回
    • 一旦process_view钩子返回Response,则中断已后的process_view调用, 直接调用中间件的返回response, 开始从最后的process_response向前执行

    中间件执行顺序

    大致程序流程图:

    中间件执行顺序

  • 其余中间件 总的中间件执行顺序如下图所示:

    中间件执行顺序

    大致程序流程图(此图中没有templates的顺序):

    中间件执行顺序

  • 另一种结构图 如果还不明白,可以参看下面的调用关系图: 中间件执行顺序

1.7. 中间件案例-利用传统方法

  • 创建项目v6_mw1

    • 设置settings.py

        INSTALLED_APPS = [
            ... ...
            'django.contrib.staticfiles',
            'tuling',
        ]
      
        MIDDLEWARE = [
            ... ...
            'django.middleware.clickjacking.XFrameOptionsMiddleware',
            'tuling.mws.MW1', #导入MW1
            'tuling.mws.MW2',
        ]
      
    • 修改v6_mw1.urls.py

        urlpatterns = [
            path('admin/', admin.site.urls),
            path('wm1/', mw1),
        ]
      
    • 修改tuling.views.py

        def mw1(r):
            print("tuling.views.mw1: 视图被执行了")
            return HttpResponse("Hello world from middleware and http://www.baoshu.red")
      
    • 添加文件tuling.mws.py

    • tuling.mws.py内容

        from django.utils.deprecation import MiddlewareMixin
      
        class MW1(MiddlewareMixin):
      
            def process_request(self,request):
                print("MW1-request")
      
            def process_response(self,request,response):
                print("MW1-response")
                return response
      
            def process_view(self, request, view_func, view_args, view_kwargs):
      
                print("MW1-view: 在{}视图前".format(view_func.__name__))
      
            def process_exception(self,request,exception):
                print("MW1-exception")
      
      
      
        class MW2(MiddlewareMixin):
      
            def process_request(self,request):
                print("MW2-request")
      
            def process_response(self,request,response):
                print("MW2-response")
                return response
      
            def process_view(self, request, view_func, view_args, view_kwargs):
      
                print("MW2-view: {}前".format(view_func.__name__))
      
            def process_exception(self,request,exception):
                print("MW2-exception")
      
    • 访问结果

      当访问http://127.0.0.1:8000/wm1/的时候:

      结果

      从运行结果已经能说明中间件的调用关系。

1.8. 自定义中间件-官方方法

Django推荐的官方写法,Django2以上都支持,2.0以下版本不清楚,没查。

这种编写方式省去了process_request()和process_response()方法的编写,将它们直接集成在一起了。

中间件本质上是一个可调用的对象(函数、方法、类),它接受一个请求(request), 并返回一个响应(response)或者None,就像视图一样。其初始化参数是一个名为get_response的可调用对象。

中间件可以被写成下面这样的函数(下面的语法,本质上是一个Python装饰器,不推荐这种写法):

    def simple_middleware(get_response):
        # 配置和初始化
        def middleware(request):
            # 在这里编写具体业务视图和随后的中间件被调用之前需要执行的代码
            response = get_response(request)
            # 在这里编写视图调用后需要执行的代码
            return response
        return middleware

或者写成一个类(真.推荐形式),这个类的实例是可调用的,魔法函数__call__需要了解下,如下所示:

    class SimpleMiddleware:
        def __init__(self, get_response):
            self.get_response = get_response
             # 配置和初始化

        def __call__(self, request):
            # 在这里编写视图和后面的中间件被调用之前需要执行的代码
            # 这里其实就是旧的process_request()方法的代码
            
            response = self.get_response(request)
            
            # 在这里编写视图调用后需要执行的代码
            # 这里其实就是旧的process_response()方法的代码
            return response

这里只把处理request和response的钩子合并,其余的正常使用。

Django仅使用 get_response 参数初始化中间件,因此不能为 init() 添加其他参数。
与每次请求都会调用 call() 方法不同,当 Web 服务器启动后,init() 只被调用一次

1.9. 中间件实例-IP拦截,用官方推荐方式

  • mws.py中继续编写code:

      from django.http import HttpResponseForbidden
      from django.conf import settings
    
      class BlackListMW():
          def __init__(self, get_response):
              self.get_response = get_response
          def __call__(self, request):
              ip = request.META['REMOTE_ADDR']
              if ip in getattr(settings, "BLACKLIST", []):
                  return HttpResponseForbidden('<h1>IP{}地址被限制访问!</h1>'.format(ip))
              response = self.get_response(request)
              response.content = "IP:{}可以访问".format(ip)
              return response
    
  • 访问后会发现页面内容变了, 如果在settings.py设置了黑名单内容,在黑名单的IP就会被禁止访问

1.10. 中间件实例-技术错误返回,官方推荐方法

  • 如果服务器发生错误,对外部显示不需要有技术细节,处于安全考虑

  • 但对内部人员应该尽可能把调试内容显示出来

  • 在显示的时候,如果不是内部人员,显示我们想给的内容,否则显示错误细节

      import sys
      from django.views.debug import technical_500_response
      from django.http import HttpResponse
      from django.conf import settings
    
      class DebugMW():
    
          def __init__(self, get_response):
              self.get_response = get_response
    
          def __call__(self, request):
              response = self.get_response(request)
              return response
    
          def process_exception(self, request, exception):
              # 如果是管理员,则返回一个特殊的响应对象,也就是Debug页面
              # 如果是普通用户,则返回None,交给默认的流程处理
              # 要求settings必须设置变量ADMIN_IP
              if request.user.is_superuser or request.META.get('REMOTE_ADDR') in settings.ADMIN_IP:
                  return technical_500_response(request, *sys.exc_info())
              else:
                  return HttpResponse("我发生了点错误,但我不告诉你是啥")