企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 第十七章: 中间件 # 第十七章: 中间件 在有些场合,需要对Django处理的每个request都执行某段代码。 这类代码可能是在view处理之前修改传入的request,或者记录日志信息以便于调试,等等。 这类功能可以用Django的中间件框架来实现,该框架由切入到Django的request/response处理过程中的钩子集合组成。 这个轻量级低层次的plug-in系统,能用于全面的修改Django的输入和输出。 每个中间件组件都用于某个特定的功能。 如果你是顺着这本书读下来的话,你应该已经多次见到“中间件”了 - 第12章中所有的session和user工具都籍由一小簇中间件实现(例如,由中间件设定view中可见的 `request.session` 和 `request.user` )。 - 第13章讨论的站点范围cache实际上也是由一个中间件实现,一旦该中间件发现与view相应的response已在缓存中,就不再调用对应的view函数。 - 第14章所介绍的 `flatpages` , `redirects` , 和 `csrf` 等应用也都是通过中间件组件来完成其魔法般的功能。 这一章将深入到中间件及其工作机制中,并阐述如何自行编写中间件。 ## 什么是中间件 我们从一个简单的例子开始。 高流量的站点通常需要将Django部署在负载平衡proxy(参见第20章)之后。 这种方式将带来一些复杂性,其一就是每个request中的远程IP地址(`request.META["REMOTE_IP"]`)将指向该负载平衡proxy,而不是发起这个request的实际IP。 负载平衡proxy处理这个问题的方法在特殊的 `X-Forwarded-For` 中设置实际发起请求的IP。 因此,需要一个小小的中间件来确保运行在proxy之后的站点也能够在 `request.META["REMOTE_ADDR"]` 中得到正确的IP地址: ``` <pre class="calibre9">``` class SetRemoteAddrFromForwardedFor(object): def process_request(self, request): try: real_ip = request.META['HTTP_X_FORWARDED_FOR'] except KeyError: pass else: # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. # Take just the first one. real_ip = real_ip.split(",")[0] request.META['REMOTE_ADDR'] = real_ip ``` ``` (Note: Although the HTTP header is called `X-Forwarded-For` , Django makes it available as `request.META['HTTP_X_FORWARDED_FOR']` . With the exception of `content-length` and `content-type` , any HTTP headers in the request are converted to `request.META` keys by converting all characters to uppercase, replacing any hyphens with underscores and adding an `HTTP_` prefix to the name.) 一旦安装了该中间件(参见下一节),每个request中的 `X-Forwarded-For` 值都会被自动插入到 `request.META['REMOTE_ADDR']` 中。这样,Django应用就不需要关心自己是否位于负载平衡proxy之后;简单读取 `request.META['REMOTE_ADDR']` 的方式在是否有proxy的情形下都将正常工作。 实际上,为针对这个非常常见的情形,Django已将该中间件内置。 它位于 `django.middleware.http` 中, 下一节将给出这个中间件相关的更多细节。 ## 安装中间件 如果按顺序阅读本书,应当已经看到涉及到中间件安装的多个示例,因为前面章节的许多例子都需要某些特定的中间件。 出于完整性考虑,下面介绍如何安装中间件。 要启用一个中间件,只需将其添加到配置模块的 `MIDDLEWARE_CLASSES` 元组中。 在 `MIDDLEWARE_CLASSES` 中,中间件组件用字符串表示: 指向中间件类名的完整Python路径。 例如,下面是 `django-admin.py startproject` 创建的缺省 `MIDDLEWARE_CLASSES` : ``` <pre class="calibre9">``` MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) ``` ``` Django项目的安装并不强制要求任何中间件,如果你愿意, `MIDDLEWARE_CLASSES` 可以为空。 这里中间件出现的顺序非常重要。 在request和view的处理阶段,Django按照 `MIDDLEWARE_CLASSES` 中出现的顺序来应用中间件,而在response和异常处理阶段,Django则按逆序来调用它们。 也就是说,Django将 `MIDDLEWARE_CLASSES` 视为view函数外层的顺序包装子: 在request阶段按顺序从上到下穿过,而在response则反过来。 ## 中间件方法 现在,我们已经知道什么是中间件和怎么安装它,下面将介绍中间件类中可以定义的所有方法。 ### Initializer: **init**(self) **init**(self)「初始化] 在中间件类中, `__init__()` 方法用于执行系统范围的设置。 出于性能的考虑,每个已启用的中间件在每个服务器进程中只初始化 *一* 次。 也就是说 `__init__()` 仅在服务进程启动的时候调用,而在针对单个request处理时并不执行。 对一个middleware而言,定义 `__init__()` 方法的通常原因是检查自身的必要性。 如果 `__init__()` 抛出异常 `django.core.exceptions.MiddlewareNotUsed` ,则Django将从middleware栈中移出该middleware。 可以用这个机制来检查middleware依赖的软件是否存在、服务是否运行于调试模式、以及任何其它环境因素。 在中间件中定义 `__init__()` 方法时,除了标准的 `self` 参数之外,不应定义任何其它参数。 ### Request预处理函数: process\_request(self, request) process\_request(self, request) 这个方法的调用时机在Django接收到request之后,但仍未解析URL以确定应当运行的view之前。 Django向它传入相应的 `HttpRequest` 对象,以便在方法中修改。 `process_request()` 应当返回 `None` 或 `HttpResponse` 对象. - 如果返回 `None` , Django将继续处理这个request,执行后续的中间件, 然后调用相应的view. - 如果返回 `HttpResponse` 对象, Django 将不再执行 *任何* 其它的中间件(而无视其种类)以及相应的view。 Django将立即返回该 `HttpResponse` . ### View预处理函数: process\_view(self, request, view, args, kwargs) process\_view(self, request, view, args, kwargs) 这个方法的调用时机在Django执行完request预处理函数并确定待执行的view之后,但在view函数实际执行之前。 表15-1列出了传入到这个View预处理函数的参数。 表 15-1\\. 传入process\_view()的参数 参数说明`request`The `HttpRequest` object.`view`The Python function that Django will call to handle this request. This is the actual function object itself, not the name of the function as a string.`args`将传入view的位置参数列表,但不包括 `request` 参数(它通常是传 入view的第一个参数) `kwargs`将传入view的关键字参数字典.Just like `process_request()` , `process_view()` should return either `None` or an `HttpResponse` object. - If it returns `None` , Django will continue processing this request, executing any other middleware and then the appropriate view. - If it returns an `HttpResponse` object, Django won’t bother calling *any* other middleware (of any type) or the appropriate view. Django will immediately return that `HttpResponse` . ### Response后处理函数: process\_response(self, request, response) process\_response(self, request, response) 这个方法的调用时机在Django执行view函数并生成response之后。 Here, the processor can modify the content of a response. One obvious use case is content compression, such as gzipping of the request’s HTML. 这个方法的参数相当直观: `request` 是request对象,而 `response` 则是从view中返回的response对象。 `request` is the request object, and `response` is the response object returned from the view. 不同可能返回 `None` 的request和view预处理函数, `process_response()` *必须* 返回 `HttpResponse` 对象. 这个response对象可以是传入函数的那一个原始对象(通常已被修改),也可以是全新生成的。 That response could be the original one passed into the function (possibly modified) or a brand-new one. ### Exception后处理函数: process\_exception(self, request, exception) process\_exception(self, request, exception) 这个方法只有在request处理过程中出了问题并且view函数抛出了一个未捕获的异常时才会被调用。 这个钩子可以用来发送错误通知,将现场相关信息输出到日志文件, 或者甚至尝试从错误中自动恢复。 这个函数的参数除了一贯的 `request` 对象之外,还包括view函数抛出的实际的异常对象 `exception` 。 `process_exception()` 应当返回 None 或 HttpResponse 对象. - 如果返回 `None` , Django将用框架内置的异常处理机制继续处理相应request。 - 如果返回 `HttpResponse` 对象, Django 将使用该response对象,而短路框架内置的异常处理机制。 备注 Django自带了相当数量的中间件类(将在随后章节介绍),它们都是相当好的范例。 阅读这些代码将使你对中间件的强大有一个很好的认识。 在Djangos wiki上也可以找到大量的社区贡献的中间件范例: <http://code.djangoproject.com/wiki/ContributedMiddleware> <http://code.djangoproject.com/wiki/ContributedMiddleware> ## 内置的中间件 Django自带若干内置中间件以处理常见问题,将从下一节开始讨论。 ### 认证支持中间件 中间件类: `django.contrib.auth.middleware.AuthenticationMiddleware` . `django.contrib.auth.middleware.AuthenticationMiddleware` . 这个中间件激活认证支持功能. 它在每个传入的 `HttpRequest` 对象中添加代表当前登录用户的 `request.user` 属性。 It adds the `request.user` attribute, representing the currently logged-in user, to every incoming `HttpRequest` object. 完整的细节请参见第12章。 ### 通用中间件 Middleware class: `django.middleware.common.CommonMiddleware` . 这个中间件为完美主义者提供了一些便利: > *禁止 `DISALLOWED_USER_AGENTS` 列表中所设置的user agent访问* :一旦提供,这一列表应当由已编译的正则表达式对象组成,这些对象用于匹配传入的request请求头中的user-agent域。 下面这个例子来自某个配置文件片段: ``` <pre class="calibre9">``` import re DISALLOWED_USER_AGENTS = ( re.compile(r'^OmniExplorer_Bot'), re.compile(r'^Googlebot') ) ``` ``` > 请注意 `import re` ,因为 `DISALLOWED_USER_AGENTS` 要求其值为已编译的正则表达式(也就是 `re.compile()` 的返回值)。 > > *依据 `APPEND_SLASH` 和 `PREPEND_WWW` 的设置执行URL重写* :如果 `APPEND_SLASH` 为 `True` , 那些尾部没有斜杠的URL将被重定向到添加了斜杠的相应URL,除非path的最末组成部分包含点号。 因此, `foo.com/bar` 会被重定向到 `foo.com/bar/` , 但是 `foo.com/bar/file.txt` 将以不变形式通过。 > > 如果 `PREPEND_WWW` 为 True , 那些缺少先导www.的URLs将会被重定向到含有先导www.的相应URL上。 will be redirected to the same URL with a leading www.. > > 这两个选项都是为了规范化URL。 其后的哲学是每个URL都应且只应当存在于一处。 技术上来说,URL `example.com/bar` 与 `example.com/bar/` 及 `www.example.com/bar/` 都互不相同。 > > *依据 `USE_ETAGS` 的设置处理Etag* : *ETags* 是HTTP级别上按条件缓存页面的优化机制。 如果 `USE_ETAGS` 为 `True` ,Django针对每个请求以MD5算法处理页面内容,从而得到Etag, 在此基础上,Django将在适当情形下处理并返回 `Not Modified` 回应(译注: > > 请注意,还有一个条件化的 `GET` 中间件, 处理Etags并干得更多,下面马上就会提及。 ### 压缩中间件 中间件类 `django.middleware.gzip.GZipMiddleware` . 这个中间件自动为能处理gzip压缩(包括所有的现代浏览器)的浏览器自动压缩返回\]内容。 这将极大地减少Web服务器所耗用的带宽。 代价是压缩页面需要一些额外的处理时间。 相对于带宽,人们一般更青睐于速度,但是如果你的情形正好相反,尽可启用这个中间件。 ### 条件化的GET中间件 Middleware class: `django.middleware.http.ConditionalGetMiddleware` . 这个中间件对条件化 `GET` 操作提供支持。 如果response头中包括 `Last-Modified` 或 `ETag` 域,并且request头中包含 `If-None-Match` 或 `If-Modified-Since` 域,且两者一致,则该response将被response 304(Not modified)取代。 对 `ETag` 的支持依赖于 `USE_ETAGS` 配置及事先在response头中设置 `ETag` 域。稍前所讨论的通用中间件可用于设置response中的 `ETag` 域。 As discussed above, the `ETag` header is set by the Common middleware. 此外,它也将删除处理 `HEAD` request时所生成的response中的任何内容,并在所有request的response头中设置 `Date` 和 `Content-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` 的情形下,这一功能将非常有用。 It sets `request.META['REMOTE_ADDR']` based on `request.META['HTTP_X_FORWARDED_FOR']` , if the latter is set. This is useful if you’re sitting behind a reverse proxy that causes each request’s `REMOTE_ADDR` to be set to `127.0.0.1` . 红色警告! 这个middleware并 *不* 验证 `HTTP_X_FORWARDED_FOR` 的合法性。 如果站点并不位于自动设置 `HTTP_X_FORWARDED_FOR` 的反向代理之后,请不要使用这个中间件。 否则,因为任何人都能够伪造 `HTTP_X_FORWARDED_FOR` 值,而 `REMOTE_ADDR` 又是依据 `HTTP_X_FORWARDED_FOR` 来设置,这就意味着任何人都能够伪造IP地址。 只有当能够绝对信任 `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` . 这个中间件将数据库的 `COMMIT` 或 `ROLLBACK` 绑定到request/response处理阶段。 如果view函数成功执行,则发出 `COMMIT` 指令。 如果view函数抛出异常,则发出 `ROLLBACK` 指令。 这个中间件在栈中的顺序非常重要。 其外层的中间件模块运行在Django缺省的 保存-提交 行为模式下。 而其内层中间件(在栈中的其后位置出现)将置于与view函数一致的事务机制的控制下。 关于数据库事务处理的更多信息,请参见附录C。 ## 下一章 Web开发者和数据库模式设计人员并不总是享有白手起家打造项目的奢侈机会。 In the next chapter, we’ll cover how to integrate with legacy systems, such as database schemas you’ve inherited from the 1980s.