中间件

Posted 虫萧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了中间件相关的知识,希望对你有一定的参考价值。

在有些场合,需要对Django处理的每个request都执行某段代码。这类代码可能是在view处理之前修改传入的request,或者记录日志信息以便于调试,等等。说的直白一点中间件是帮助我们在视图函数执行之前和执行之后都可以做一些额外的操作,它本质上就是一个自定义类,类中定义了几个方法,Django框架会在请求的特定的时间去执行这些方法。

这类功能可以用Django的中间件框架来实现,该框架由切入到Django的request/response处理过程中的钩子集合组成。这个轻量级低层次的plug-in系统,能用于全面的修改Django的输入和输出。

打开Django项目的Settings.py文件,看到下面的MIDDLEWARE配置项,django默认自带的一些中间件:

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',
]

MIDDLEWARE配置项是一个列表,列表中是一个个字符串,这些字符串其实是一个个类,也就是一个个中间件。

例如有csrf相关的中间件,如果把它注释掉,再提交post请求的时候,就不会被forbidden了。

自定义中间件示例

中间件可以定义五个方法,分别是:(主要的是process_request和process_response)

  • process_request(self,request)

  • process_view(self,request,view_func,view_args,view_kwargs)

  • process_template_response(self,request,response)

  • process_exception(self, request, exception)

  • process_response(self, request, response)

以上方法的返回值可以是None或一个HttpResponse对象,如果是None,则继续按照django定义的规则向后继续执行,如果是HttpResponse对象,则直接将该对象返回给用户。

我们也可以自己定义一个中间件,写一个类,但是必须继承MiddlewareMixin

目录:在项目(或app中)中创建一个包,一般都放在一个叫做utils的包里面,表示一个公用的组件,创建一个py文件,随便起名字,例如叫做:middlewares.py,内容如下:

from django.utils.deprecation import MiddlewareMixin

class MD1(MiddlewareMixin):
    # 自定义中间件,不是必须要有下面这两个方法,有request方法说明请求来了要处理,有response方法说明响应出去时需要处理,不是非要写这两个方法,如果你没写process_response方法,那么会一层一层的往上找,哪个中间件有process_response方法就将返回对象给哪个中间件
    def process_request(self, request):
        print("MD1里面的 process_request")

    def process_response(self, request, response):
        print("MD1里面的 process_response")
        return response

注册自定义中间件

要启用一个中间件,只需将其添加到在settings.py的配置模块的 MIDDLEWARE 列表中。在 MIDDLEWARE 中,中间件组件用字符串表示:指向中间件类名的完整Python路径。

MIDDLEWARE = [
     ...
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'utils.middlewares.MD1',  # 自定义中间件MD1,是项目路径下的一个路径。
]

Django项目的安装并不强制要求任何中间件,MIDDLEWARE 可以为空。

中间件出现的顺序:在request和view的处理阶段,Django按照 MIDDLEWARE 中出现的顺序来应用中间件,而在response和异常处理阶段,Django则按逆序来调用它们。即第一个中间件的process_request方法首先执行,而它的process_response方法最后执行,最后一个中间件的process_request方法最后一个执行,它的process_response方法是最先执行。且不同中间件之间传递的request都是同一个对象。

中间件方法

Request预处理函数: process_request(self, request)

这个方法的调用时机在Django接收到request之后,但仍未解析URL以确定应当运行的view之前。Django向它传入相应的 HttpRequest 对象,以便在方法中修改。

process_request() 应当返回 NoneHttpResponse 对象.

  • 如果返回 None ,Django将继续处理这个request,执行后续的中间件, 然后调用相应的view。

  • 如果返回 HttpResponse 对象,Django 将不再执行任何其它的中间件(而无视其种类)以及相应的view。Django将给浏览器立即返回该 HttpResponse 对象。

Response后处理函数: process_response(self,request,response)

这个方法的调用时机在Django执行view函数并生成response之后。参数 request 是request对象,而 response 则是从view中返回的response对象。

process_response()必须返回 HttpResponse 对象。这个response对象可以是传入函数的那一个原始对象(通常已被修改),也可以是全新生成的。

from django.utils.deprecation import MiddlewareMixin


class MD1(MiddlewareMixin):

    def process_request(self, request):
        print("MD1里面的 process_request")
        # 不必须写return值
    def process_response(self, request, response):
        print("MD1里面的 process_response")        # print(response.__dict__['_container'][0].decode('utf-8')),查看响应体里面的内容的方法,或者直接使用response.content也可以看到响应体里面的内容,由于response是个变量,直接点击看源码是看不到的,你打印type(response)发现是HttpResponse对象。
     return response  #必须有返回值,不然你上层的中间件就没有拿到httpresponse对象,就会报错
        # return HttpResponse('haha') ,如果你写了这个,那么你视图返回过来的内容就被它给替代了

自定义中间之后,注意中间件的摆放顺序:之前做的cookie认证,都是通过在函数上面加装饰器,比较麻烦,可以用中间件,如果是session认证的,必须放在django自带的session中间件的下面。

View预处理函:process_view(self,request,view_func, view_args, view_kwargs)

这个方法的调用时机在Django执行完request预处理函数并确定待执行的view之后,但在view函数实际执行之前。简单说即Django会在调用视图函数之前调用process_view方法。

参数 说明
request The HttpRequest object.
view_func Django即将使用的视图函数。它是实际的函数对象,而不是函数的名称作为字符串。
views_args 将传递给视图的位置参数的列表。
views_kwargs 将传入view的关键字参数字典.

view_args和view_kwargs都不包含第一个视图参数(request)。

它应该返回None或一个HttpResponse对象。如果返回None,Django将继续处理这个请求,执行任何其他中间件的process_view方法,然后在执行相应的视图。如果它返回一个HttpResponse对象,Django不会调用对应的视图函数。它将执行中间件的process_response方法并将应用到该HttpResponse并返回结果。

中间件

Exception后处理函数: process_exception(self, request, exception)

这个方法只有在request处理过程中出了问题并且view函数抛出了一个未捕获的异常时才会被调用。这个钩子可以用来发送错误通知,将现场相关信息输出到日志文件, 或者甚至尝试从错误中自动恢复。

这个函数的参数除 request 对象之外,还包括view函数抛出的实际的异常对象 exception

process_exception() 应当返回 None 或 HttpResponse 对象.

  • 如果返回 None ,则交给下一个中间件的process_exception方法来处理异常,Django框架有内置的异常处理机制。

  • 如果返回 HttpResponse 对象, Django将调用模板和中间件中的process_response方法,并返回给浏览器,而短路框架内置的异常处理机制。

  • 它的执行顺序也是按照中间件注册顺序的倒序执行。

中间件

process_template_response(self, request, response)

它的参数,一个HttpRequest对象,response是TemplateResponse对象(由视图函数或者中间件产生)。

在视图函数执行完成后立即执行,但是它有一个前提条件,那就是视图函数返回的对象有一个render()方法(或者表明该对象是一个TemplateResponse对象或等价方法)。

views.py中:

def index(request):
    print("app01 中的 index视图")
  #raise ValueError('出错啦') 
    def render():
        print("in index/render")          #raise ValueError('出错啦') #至于render函数中报错了,那么会先执行process_template_response方法,然后执行process_exception方法,如果是在render方法外面报错了,那么就不会执行这个process_template_response方法了。
        return HttpResponse("O98K"# 返回的将是这个新的对象,并将下面语句中的HttpResponse对象顶替掉。
    rep = HttpResponse("OK")
    rep.render = render
    return rep

render函数中报错了,那么会先执行process_template_response方法,然后执行process_exception方法,如果是在render方法外面报错了,那么就不会执行这个process_template_response方法了。

Initializer: init(self) init(self)「初始化]

在中间件类中, __init__() 方法用于执行系统范围的设置。

出于性能的考虑,每个已启用的中间件在每个服务器进程中只初始化 次。也就是说 __init__() 仅在服务进程启动的时候调用,而在针对单个request处理时并不执行。

对一个middleware而言,定义 __init__() 方法的通常原因是检查自身的必要性。如果 __init__() 抛出异常 django.core.exceptions.MiddlewareNotUsed ,则Django将从middleware栈中移出该middleware。可以用这个机制来检查middleware依赖的软件是否存在、服务是否运行于调试模式、以及任何其它环境因素。

在中间件中定义 __init__() 方法时,除了标准的 self 参数之外,不应定义任何其它参数。

在Djangos wiki上可以找到大量的社区贡献的中间件范例: http://code.djangoproject.com/wiki/ContributedMiddlewarehttp://code.djangoproject.com/wiki/ContributedMiddleware

中间件执行流程

请求到达中间件之后,先按照正序执行每个注册中间件的process_reques方法,process_request方法返回的值是None,就依次执行,如果返回的值是HttpResponse对象,不再执行后面的process_request方法,而是执行当前对应中间件的process_response方法,将HttpResponse对象返回给浏览器。

如果MIDDLEWARE中注册了6个中间件,执行过程中,第3个中间件返回了一个HttpResponse对象,那么第4,5,6中间件的process_request和process_response方法都不执行,顺序执行3,2,1中间件的process_response方法。

中间件

process_request方法都执行完后,匹配路由,找到要执行的视图函数,先不执行视图函数,先执行中间件中的process_view方法,process_view方法返回None,继续按顺序执行,所有process_view方法执行完后执行视图函数。假如中间件3 的process_view方法返回了HttpResponse对象,则4,5,6的process_view以及视图函数都不执行,直接从最后一个中间件,也就是中间件6的process_response方法开始倒序执行。

内置的中间件

认证支持中间件

中间件类: django.contrib.auth.middleware.AuthenticationMiddleware . django.contrib.auth.middleware.AuthenticationMiddleware .

这个中间件激活认证支持功能. 它在每个传入的 HttpRequest 对象中添加代表当前登录用户的 request.user 属性。

通用中间件

Middleware class: django.middleware.common.CommonMiddleware .

这个中间件为完美主义者提供了一些便利:

禁止 DISALLOWED_USER_AGENTS 列表中所设置的user agent访问 :一旦提供,这一列表应当由已编译的正则表达式对象组成,这些对象用于匹配传入的request请求头中的user-agent域。下面这个例子来自某个配置文件片段:

import re

DISALLOWED_USER_AGENTS = (
    re.compile(r'^OmniExplorer_Bot'),
    re.compile(r'^Googlebot')
)

请注意 import re ,因为 DISALLOWED_USER_AGENTS 要求其值为已编译的正则表达式(也就是 re.compile() 的返回值)。

依据 APPEND_SLASHPREPEND_WWW 的设置执行URL重写 :如果 APPEND_SLASHTrue , 那些尾部没有斜杠的URL将被重定向到添加了斜杠的相应URL,除非path的最末组成部分包含点号。因此, foo.com/bar 会被重定向到 foo.com/bar/ , 但是 foo.com/bar/file.txt 将以不变形式通过。

如果 PREPEND_WWW 为 True , 那些缺少先导www.的URLs将会被重定向到含有先导www.的相应URL上。

这两个选项都是为了规范化URL。其后的哲学是每个URL都应且只应当存在于一处。技术上来说,URL example.com/barexample.com/bar/www.example.com/bar/ 都互不相同。

依据 USE_ETAGS 的设置处理Etag : ETags 是HTTP级别上按条件缓存页面的优化机制。如果 USE_ETAGSTrue ,Django针对每个请求以MD5算法处理页面内容,从而得到Etag, 在此基础上,Django将在适当情形下处理并返回 Not Modified 回应(译注:

请注意,还有一个条件化的 GET 中间件, 处理Etags并干得更多,下面马上就会提及。

压缩中间件

中间件类 django.middleware.gzip.GZipMiddleware .

这个中间件自动为能处理gzip压缩(包括所有的现代浏览器)的浏览器自动压缩返回]内容。这将极大地减少Web服务器所耗用的带宽。代价是压缩页面需要一些额外的处理时间。

相对于带宽,人们一般更青睐于速度,但是如果你的情形正好相反,尽可启用这个中间件。

条件化的GET中间件

Middleware class: django.middleware.http.ConditionalGetMiddleware .

这个中间件对条件化 GET 操作提供支持。如果response头中包括 Last-ModifiedETag 域,并且request头中包含 If-None-MatchIf-Modified-Since 域,且两者一致,则该response将被response 304(Not modified)取代。对 ETag 的支持依赖于 USE_ETAGS 配置及事先在response头中设置 ETag 域。稍前所讨论的通用中间件可用于设置response中的 ETag 域。

此外,它也将删除处理 HEAD request时所生成的response中的任何内容,并在所有request的response头中设置 DateContent-Length 域。

反向代理支持 (X-Forwarded-For中间件)

Middleware class: django.middleware.http.SetRemoteAddrFromForwardedFor .

这是我们在 什么是中间件 这一节中所举的例子。在 request.META['HTTP_X_FORWARDED_FOR'] 存在的前提下,它根据其值来设置 request.META['REMOTE_ADDR'] 。在站点位于某个反向代理之后的、每个request的 REMOTE_ADDR 都被指向 127.0.0.1 的情形下,这一功能将非常有用。

这个middleware并不验证 HTTP_X_FORWARDED_FOR 的合法性。

只有当能够绝对信任 HTTP_X_FORWARDED_FOR 值的时候才能够使用这个中间件。

会话支持中间件

Middleware class: django.contrib.sessions.middleware.SessionMiddleware .

这个中间件激活会话支持功能. 细节请参见第12章。See Chapter 14 for details.

站点缓存中间件

Middleware classes: django.middleware.cache.UpdateCacheMiddleware and django.middleware.cache.FetchFromCacheMiddleware .

这些中间件互相配合以缓存每个基于Django的页面。已在第13章中详细讨论。

事务处理中间件

Middleware class: django.middleware.transaction.TransactionMiddleware .

这个中间件将数据库的 COMMITROLLBACK 绑定到request/response处理阶段。如果view函数成功执行,则发出 COMMIT 指令。如果view函数抛出异常,则发出 ROLLBACK 指令。

这个中间件在栈中的顺序非常重要。其外层的中间件模块运行在Django缺省的 保存-提交 行为模式下。而其内层中间件(在栈中的其后位置出现)将置于与view函数一致的事务机制的控制下。

中间件版登陆认证

中间件版的登录验证需要依靠session,所以数据库中要有django_session表。

urls.py:

from django.conf.urls import url
from app01 import views

urlpatterns = [
    path('index/', views.index),
    path('login/', views.login, name='login'),
]

views.py

from django.shortcuts import render, HttpResponse, redirect


def index(request):
    return HttpResponse('this is index')


def home(request):
    return HttpResponse('this is home')


def login(request):
    if request.method == "POST":
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")

        if user == "haha" and pwd == "123456":
            # 设置session
            request.session["user"] = user
            # 获取跳到登陆页面之前的URL
            next_url = request.GET.get("next")
            # 如果有,就跳转回登陆之前的URL
            if next_url:
                return redirect(next_url)
            # 否则默认跳转到index页面
            else:
                return redirect("/index/")
    return render(request, "login.html")

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>登录页面</title>
</head>
<body>
<form action="{% url 'login' %}">
    <p>
        <label for="user">用户名:</label>
        <input type="text" name="user" id="user">
    </p>
    <p>
        <label for="pwd">密 码:</label>
        <input type="text" name="pwd" id="pwd">
    </p>
    <input type="submit" value="登录">
</form>
</body>
</html>

middlewares.py

class AuthMD(MiddlewareMixin):
    white_list = ['/login/', ]  # 白名单
    balck_list = ['/black/', ]  # 黑名单

    def process_request(self, request):
        from django.shortcuts import redirect, HttpResponse

        next_url = request.path_info
        print(request.path_info, request.get_full_path())

        if next_url in self.white_list or request.session.get("user"):
            return
        elif next_url in self.balck_list:
            return HttpResponse('This is an illegal URL')
        else:
            return redirect("/login/?next={}".format(next_url))

在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',
    'middlewares.AuthMD',
]

AuthMD中间件注册后,所有的请求都要走AuthMD的process_request方法。访问的URL在白名单内或者session中有user用户名,则不做阻拦走正常流程;如果URL在黑名单中,则返回This is an illegal URL的字符串;正常的URL但是需要登录后访问,让浏览器跳转到登录页面。

注:AuthMD中间件中需要session,所以AuthMD注册的位置要在session中间的下方。

Django请求流程图:


以上是关于中间件的主要内容,如果未能解决你的问题,请参考以下文章

Sublime Text3自定义代码片段

无法更新 View Pager 中的中间片段。

Android - 从堆栈中间删除特定片段不起作用

从没有中间转换变量的片段中观察 ViewModel LiveData

如何将数据从顶点着色器传递到片段着色器,中间有着色器[重复]

如何定义对话框片段的重力?