1. 程式人生 > >Django中間件詳解

Django中間件詳解

關系 響應 的人 中間件 messages elf 對象 exception 繼承

一.中間件概念  

   中間件顧名思義,是介於request與response處理之間的一道處理過程,相對比較輕量級,並且在全局上改變django的輸入與輸出。因為改變的是全局,所以需要謹慎實用,用不好會影響到性能。我們從瀏覽器發出一個請求 Request,得到一個響應後的內容 HttpResponse ,這個請求傳遞到 Django的過程如下:

            技術分享圖片

  

   也就是說,每一個請求都是先通過中間件中的 process_request 函數,這個函數返回 None 或者 HttpResponse 對象,如果返回前者,繼續處理其它中間件,如果返回一個 HttpResponse,就處理中止,返回到網頁上。

中間件不用繼承自任何類(可以繼承 object ),下面一個中間件大概的樣子:

class CommonMiddleware(object):
    def process_request(self, request):
        return None

    def process_response(self, request, response):
        return response

   除了上面兩個經常使用的函數外,還有 process_view, process_exception 和 process_template_response 函數。因此中間件中經常使用的函數就是如上的五種。而且中間件隨著Django的版本不同也會不同,這點我們將在下面具體討論。

二.實際應用需求:

    比如我們要做一個 攔截器,發現有惡意訪問網站的人,就攔截他!假如我們通過一種技術,比如統計一分鐘訪問頁面的次數,太多就把他的 IP 加入到黑名單 BLOCKED_IPS(這部分沒有提供代碼,主要講中間件部分):

這裏的代碼的功能就是 獲取當前訪問者的 IP (request.META[REMOTE_ADDR]),如果這個 IP 在黑名單中就攔截,如果不在就返回 None (函數中沒有返回值其實就是默認為 None),把這個中間件的 Python 路徑寫到settings.py中即可生效,這裏只是舉一個簡單的例子,後面會詳細舉例:
class BlockedIpMiddleware(object):
    
def process_request(self, request): # 這裏使用反射的方式拿到settings.py文件中的BLOCKED_IPS,其是一個列表,如果獲取不到 # 那就返回一個空列表 """ 因此最終實現的效果就是:BLOCKED_IPS = [‘192.168.4.128‘, ‘192.168.4.129‘, ‘192.168.4.130‘] if ‘192.168.4.129‘ in [‘192.168.4.128‘, ‘192.168.4.129‘, ‘192.168.4.130‘]: return http.HttpResponseForbidden(‘<h1>Forbidden</h1>‘) 如果獲取不到BLOCKED_IPS,那就返回一個空列表: if ‘192.168.4.129‘ in []: 這就不滿足條件,所以不執行 """ if request.META[REMOTE_ADDR] in getattr(settings, "BLOCKED_IPS", []): return http.HttpResponseForbidden(<h1>Forbidden</h1>)

1.1 Django 1.9 和以前的版本的中間件如下:

MIDDLEWARE_CLASSES = (  # 重點關註這裏的名稱
    django.contrib.sessions.middleware.SessionMiddleware,
    django.middleware.common.CommonMiddleware,
    django.middleware.csrf.CsrfViewMiddleware,
    django.contrib.auth.middleware.AuthenticationMiddleware,
    django.contrib.auth.middleware.SessionAuthenticationMiddleware,
    django.contrib.messages.middleware.MessageMiddleware,
    django.middleware.clickjacking.XFrameOptionsMiddleware,
    django.middleware.security.SecurityMiddleware,
    ‘MiddlewareProject.Middleware.my_middleware_dd.BlockedIpMiddleware‘ # 自定義的中間件
)

1.2 Django 1.10 版本 更名為 MIDDLEWARE(單復同形),寫法也有變化,部署的時候要重新修改名字

如果用 Django 1.10版本開發,部署時用 Django 1.9版本或更低版本,要特別小心此處。

MIDDLEWARE = (
    django.contrib.sessions.middleware.SessionMiddleware,
    django.middleware.common.CommonMiddleware,
    django.middleware.csrf.CsrfViewMiddleware,
    django.contrib.auth.middleware.AuthenticationMiddleware,
    django.contrib.auth.middleware.SessionAuthenticationMiddleware,
    django.contrib.messages.middleware.MessageMiddleware,
    django.middleware.clickjacking.XFrameOptionsMiddleware,
    django.middleware.security.SecurityMiddleware,
    MiddlewareProject.Middleware.my_middleware_dd.BlockedIpMiddleware
)

Django 會從 MIDDLEWARE_CLASSES 或 MIDDLEWARE 中按照從上到下的順序一個個執行中間件中的 process_request 函數,而其中 process_response 函數則是最前面的最後執行,關於具體的執行順序筆者將在後面做一個詳細的總結。

二,再比如,我們在網站放到服務器上正式運行後,DEBUG改為了 False,這樣更安全,但是有時候發生錯誤我們不能看到錯誤詳情,調試不方便,有沒有辦法處理好這兩個事情呢?普通訪問者看到的是友好的報錯信息;管理員看到的是錯誤詳情,以便於修復 BUG;當然可以有,利用中間件就可以做到!代碼如下:

import sys
from django.views.debug import technical_500_response
from django.conf import settings
 
class UserBasedExceptionMiddleware(object):
    def process_exception(self, request, exception):
        if request.user.is_superuser or request.META.get(REMOTE_ADDR) in settings.INTERNAL_IPS:
            return technical_500_response(request, *sys.exc_info())

把這個中間件像上面一樣,加到你的 settings.py 中的 MIDDLEWARE_CLASSES 中,可以放到最後,這樣可以看到其它中間件的 process_request的錯誤。

當訪問者為管理員時,就給出錯誤詳情,比如訪問本站的不存在的頁面:http://www.ziqiangxuetang.com/admin/

作為一個普通的訪問者,我們看到的是一個比較友好的普通的提示信息:

技術分享圖片

三.補充:Django 1.10 接口發生變化,變得更加簡潔

class SimpleMiddleware(object):
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.
 
    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        # 調用 view 之前的代碼
 
        response = self.get_response(request)
 
        # Code to be executed for each request/response after
        # the view is called.
        # 調用 view 之後的代碼
 
        return response

Django 1.10.x 也可以用函數來實現中間件,詳見官方文檔:https://docs.djangoproject.com/en/1.10/topics/http/middleware/#writing-your-own-middleware

四.寫出一個兼容Django各版本的中間件

try:
    from django.utils.deprecation import MiddlewareMixin  # Django 1.10.x
except ImportError:
    MiddlewareMixin = object  # Django 1.4.x - Django 1.9.x
 
 
class SimpleMiddleware(MiddlewareMixin):
    def process_request(self, request):
        pass
 
    def process_response(request, response):
        pass

新版本中 django.utils.deprecation.MiddlewareMixin 的 源代碼 如下:

class MiddlewareMixin(object):
    def __init__(self, get_response=None):
        self.get_response = get_response
        super(MiddlewareMixin, self).__init__()
 
    def __call__(self, request):
        response = None
        if hasattr(self, process_request):
            response = self.process_request(request)
        if not response:
            response = self.get_response(request)
        if hasattr(self, process_response):
            response = self.process_response(request, response)
        return response

__call__ 方法會先調用 self.process_request(request),接著執行 self.get_response(request) 然後調用 self.process_response(request, response).舊版本(Django 1.4.x-Django 1.9.x) 的話,和原來一樣。

六.實例驗證

   在講解中間件時,一個Django項目的起始執行如下:先走WSGI,然後再走中間件,註意一點,自帶的中間件的順序是不能倒置的,因為有可能下一個中間件要依賴於上一個中間件的數據,如果隨意顛倒順序,會報錯,我們通常將自定義的中間件放到最下面,本次我們使用的是Django 1.8版本:

MIDDLEWARE_CLASSES = (
    django.contrib.sessions.middleware.SessionMiddleware,
    django.middleware.common.CommonMiddleware,
    django.middleware.csrf.CsrfViewMiddleware,
    django.contrib.auth.middleware.AuthenticationMiddleware,
    django.contrib.auth.middleware.SessionAuthenticationMiddleware,
    django.contrib.messages.middleware.MessageMiddleware,
    django.middleware.clickjacking.XFrameOptionsMiddleware,
    django.middleware.security.SecurityMiddleware,
   # 自定義中間件
MiddlewareDemo.Middleware.my_middleware_dd.MiddleWare1‘, ‘MiddlewareDemo.Middleware.my_middleware_dd.MiddleWare2‘ ) """ 註意在上面,如果自定義的中間件my_middleware.py放在項目根目錄下,那麽在settings中設置的路徑直接就是my_middleware.MiddleWare1 Django能識別的只是根目錄,我們通常會自定義一個目錄,專門用來存放自定義的中間件 """

接下來,我們一起看看中間件的執行流程,來看一張簡單的圖:

技術分享圖片

通過上圖,我們可以總結出如下的流程:

request請求經過WSGI後,先進入中間件,依然開始先走process_request函數,然後走路由關系映射後,這裏註意
並沒有直接進入視圖函數,而是從頭開始執行process_view()函數;然後再去執行與urls.py匹配的視圖函數;
如果視圖函數沒有報錯,那就直接挨個反過來從最後一個中間件開始,依次將返回的實例對象(也就是我們在視圖函數中
寫的 return HttpResponse()等等)傳遞給每個中間件的process_response函數;最後再交給客戶端瀏覽器;
如果執行視圖函數出錯,那就反過來從最後一個中間件開始,將錯誤信息傳遞給每個中間件的process_exception()函數,走完所有後,然後最終再走procss_response後,最終再交給客戶端瀏覽器
註意:視圖函數的錯誤是由process_exception()函數處理的,從最後一個中間件開始,依次逐級提交捕捉到的異常
然後最終交給procss_response()函數,將最終的錯誤信息交給客戶端瀏覽器。

process_view的作用和特殊作用在哪?

走process_request的時候不知道走url中的哪個視圖函數,當我們再回來走process_view的時候,由於此時已經走了
urls.py文件,所以它已經知道該執行哪個視圖函數了我們發現process_view的源碼中它的參數多了個callback這個參數的
值就是具體的我們要執行視圖函數;因此我們可以總結其實在process_view中可以執行下我們的視圖函數。
def process_view(self, request, callback, callback_args, callback_kwargs):
這裏的callback就是視圖函數的名稱,因此如果有特殊需求,既不想進入views.py文件中執行視圖函數,但是又想在中間件層面
執行下視圖函數,可以在process_view中,直接調用視圖函數的名稱即可執行:index()

我們首先來看下面的實際例子:

自定義的中間件如下:

# _*_ coding:utf-8 _*_

try:
    from django.utils.deprecation import MiddlewareMixin  # Django 1.10.x
except ImportError:
    MiddlewareMixin = object

"""
process_request是不需要加return的,我們觀察,如果假如return
會發生什麽
"""

class MiddleWare1(object):
    def process_request(self, request):
        print("MQ1  request=======>")

    def process_response(self, request, response):
        print("MQ1  response=======>")
        """
        這裏一定要返回response,作為中間件的返回
        如果不加瀏覽器會報錯:
        MiddleWare1.process_response didn‘t return an HttpResponse object. 
        It returned None instead.
        也就是說process_response一定要返回一個response
        這裏的response其實質就是我們在視圖函數中返回給瀏覽器的返回值:
        return HttpResponse("OK")
        也就是說,視圖函數將HttpResponse("OK")作為一個參數傳遞給中間件的
        process_response()函數,然後由中間件一層層地往上面傳遞給其他中間件
        最終顯示給客戶端瀏覽器;所以是先打印view is running,然後再打印
        MQ1 response
        """
        return response

視圖函數如下:

from django.shortcuts import render, HttpResponse

# Create your views here.

def index(request):
    print("view index is running")

    return HttpResponse("index..")

最終打印結果:

MQ1  request=======>
view index is running
MQ1  response=======>

終端的打印結果:

技術分享圖片

結果印證了我們上面的執行流程,先走process_request()函數,然後再走視圖函數,最後再走process_response()函數
所以是先打印view is running,然後再打印MQ1 response。

接著wo‘men

技術分享圖片

Django中間件詳解