# 第十七章: 中間件
# 第十七章: 中間件
在有些場合,需要對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.