Django2.2中間件詳解

中間件是 Django 用來處理請求和響應的鉤子框架。它是一個輕量級的、底層級的「插件」系統,用於全局性地控制Django 的輸入或輸出,能夠理解爲內置的app或者小框架。python

django.core.handlers.base模塊中定義瞭如何接入中間件,這也是學習Django源碼的入口之一。django

每一箇中間件組件負責實現一些特定的功能。例如,Django 包含一箇中間件組件 AuthenticationMiddleware,它使用會話機制將用戶與請求request關聯起來。瀏覽器

中間件能夠放在你的工程的任何地方,並以Python路徑的方式進行訪問。緩存

Django 具備一些內置的中間件,並自動開啓了其中的一部分,咱們能夠根據本身的須要進行調整。安全

1、如何啓用中間件

若要啓用中間件組件,請將其添加到 Django 配置文件settings.pyMIDDLEWARE 配置項列表中。服務器

MIDDLEWARE 中,中間件由字符串表示。這個字符串以圓點分隔,指向中間件工廠的類或函數名的完整 Python 路徑。下面是使用 django-admin startproject命令建立工程後,默認的中間件配置:cookie

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 配置項能夠爲空。可是強烈建議至少使用 CommonMiddleware。而筆者的建議是保持默認的配置,這有助於你提升網站的安全性。網絡

2、 中間件最關鍵的順序問題

MIDDLEWARE 的順序很重要,具備前後關係,由於有些中間件會依賴其餘中間件。例如: AuthenticationMiddleware 須要在會話中間件中存儲的通過身份驗證的用戶信息,所以它必須在 SessionMiddleware 後面運行 。session

在請求階段,調用視圖以前,Django 按照定義的順序執行中間件 MIDDLEWARE,自頂向下。app

你能夠把它想象成一個洋蔥:每一箇中間件類都是一個「皮層」,它包裹起了洋蔥的核心--實際業務視圖。若是請求經過了洋蔥的全部中間件層,一直到內核的視圖,那麼響應將在返回的過程當中以相反的順序再經過每一箇中間件層,最終返回給用戶

若是某個層的執行過程認爲當前的請求應該被拒絕,或者發生了某些錯誤,致使短路,直接返回了一個響應,那麼剩下的中間件以及核心的視圖函數都不會被執行。

3、Django內置的中間件

Django內置了下面這些中間件,知足了咱們通常的需求:

Cache

緩存中間件

若是啓用了該中間件,Django會以CACHE_MIDDLEWARE_SECONDS 配置的參數進行全站級別的緩存。

Common

通用中間件

該中間件爲咱們提供了一些便利的功能:

  • 禁止DISALLOWED_USER_AGENTS中的用戶代理訪問服務器

  • 自動爲URL添加斜槓後綴和www前綴功能。若是配置項 APPEND_SLASHTrue ,而且訪問的URL 沒有斜槓後綴,在URLconf中沒有匹配成功,將自動添加斜槓,而後再次匹配,若是匹配成功,就跳轉到對應的url。 PREPEND_WWW 的功能相似。

  • 爲非流式響應設置Content-Length頭部信息。

做爲展現的例子,這裏額外貼出它的源代碼,位於django.middleware.common模塊中,比較簡單,很容易讀懂和理解:

class CommonMiddleware(MiddlewareMixin):
    """
    去掉了doc
    """
    response_redirect_class = HttpResponsePermanentRedirect

    def process_request(self, request):
        # Check for denied User-Agents
        if 'HTTP_USER_AGENT' in request.META:
            for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
                if user_agent_regex.search(request.META['HTTP_USER_AGENT']):
                    raise PermissionDenied('Forbidden user agent')

        # Check for a redirect based on settings.PREPEND_WWW
        host = request.get_host()
        must_prepend = settings.PREPEND_WWW and host and not host.startswith('www.')
        redirect_url = ('%s://www.%s' % (request.scheme, host)) if must_prepend else ''

        # Check if a slash should be appended
        if self.should_redirect_with_slash(request):
            path = self.get_full_path_with_slash(request)
        else:
            path = request.get_full_path()

        # Return a redirect if necessary
        if redirect_url or path != request.get_full_path():
            redirect_url += path
            return self.response_redirect_class(redirect_url)

    def should_redirect_with_slash(self, request):

        if settings.APPEND_SLASH and not request.path_info.endswith('/'):
            urlconf = getattr(request, 'urlconf', None)
            return (
                not is_valid_path(request.path_info, urlconf) and
                is_valid_path('%s/' % request.path_info, urlconf)
            )
        return False

    def get_full_path_with_slash(self, request):

        new_path = request.get_full_path(force_append_slash=True)
        if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
            raise RuntimeError(
                "You called this URL via %(method)s, but the URL doesn't end "
                "in a slash and you have APPEND_SLASH set. Django can't "
                "redirect to the slash URL while maintaining %(method)s data. "
                "Change your form to point to %(url)s (note the trailing "
                "slash), or set APPEND_SLASH=False in your Django settings." % {
                    'method': request.method,
                    'url': request.get_host() + new_path,
                }
            )
        return new_path

    def process_response(self, request, response):
        # If the given URL is "Not Found", then check if we should redirect to
        # a path with a slash appended.
        if response.status_code == 404:
            if self.should_redirect_with_slash(request):
                return self.response_redirect_class(self.get_full_path_with_slash(request))

        if settings.USE_ETAGS and self.needs_etag(response):
            warnings.warn(
                "The USE_ETAGS setting is deprecated in favor of "
                "ConditionalGetMiddleware which sets the ETag regardless of "
                "the setting. CommonMiddleware won't do ETag processing in "
                "Django 2.1.",
                RemovedInDjango21Warning
            )
            if not response.has_header('ETag'):
                set_response_etag(response)

            if response.has_header('ETag'):
                return get_conditional_response(
                    request,
                    etag=response['ETag'],
                    response=response,
                )
        # Add the Content-Length header to non-streaming responses if not
        # already set.
        if not response.streaming and not response.has_header('Content-Length'):
            response['Content-Length'] = str(len(response.content))

        return response

    def needs_etag(self, response):
        """Return True if an ETag header should be added to response."""
        cache_control_headers = cc_delim_re.split(response.get('Cache-Control', ''))
        return all(header.lower() != 'no-store' for header in cache_control_headers)

GZip

內容壓縮中間件

用於減少響應體積,下降帶寬壓力,提升傳輸速度。

該中間件必須位於其它全部須要讀寫響應體內容的中間件以前。

若是存在下面狀況之一,將不會壓縮響應內容:

  • 內容少於200 bytes

  • 已經設置了 Content-Encoding 頭部屬性

  • 請求的 Accept-Encoding 頭部屬性未包含 gzip.

可使用 gzip_page()裝飾器,爲視圖單獨開啓GZip壓縮服務。

Conditional GET

有條件的GET訪問中間件,不多使用。

Locale

本地化中間件

用於處理國際化和本地化,語言翻譯。

Message

消息中間件

基於cookie或者會話的消息功能,比較經常使用。

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

Session

會話中間件,很是經常使用。

Site

站點框架。

這是一個頗有用,但又被忽視的功能。

它可讓你的Django具有多站點支持的功能。

經過增長一個site屬性,區分當前request請求訪問的對應站點。

無需多個IP或域名,無需開啓多個服務器,只須要一個site屬性,就能搞定多站點服務。

Authentication

認證框架

Django最主要的中間件之一,提供用戶認證服務。

CSRF protection

提供CSRF防護機制的中間件

X-Frame-Options

點擊劫持防護中間件

4、自定義中間件

有時候,爲了實現一些特定的需求,咱們可能須要編寫本身的中間件。

在編寫方式上,須要注意的是,當前Django版本2.2,存在兩種編寫的方式。一種是Django當前官網上提供的例子,一種是老版本的方式。本質上,兩種方式實際上是同樣的。

咱們先看一下傳統的,也是技術文章最多,目前使用最多的方式。

傳統的方法

五大鉤子函數

傳統方式自定義中間件其實就是在編寫五大鉤子函數:

  • process_request(self,request)

  • process_response(self, request, response)

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

  • process_exception(self, request, exception)

  • process_template_response(self,request,response)

能夠實現其中的任意一個或多個!

鉤子函數 執行時機 執行順序 返回值
process_request 請求剛到來,執行視圖以前 配置列表的正序 None或者HttpResponse對象
process_response 視圖執行完畢,返回響應時 逆序 HttpResponse對象
process_view process_request以後,路由轉發到視圖,執行視圖以前 正序 None或者HttpResponse對象
process_exception 視圖執行中發生異常時 逆序 None或者HttpResponse對象
process_template_response 視圖剛執行完畢,process_response以前 逆序 實現了render方法的響應對象
process_request()

簽名:process_request(request)

最主要的鉤子!

只有一個參數,也就是request請求內容,和視圖函數中的request是同樣的。全部的中間件都是一樣的request,不會發生變化。它的返回值能夠是None也能夠是HttpResponse對象。返回None的話,表示一切正常,繼續走流程,交給下一個中間件處理。返回HttpResponse對象,則發生短路,不繼續執行後面的中間件,也不執行視圖函數,而將響應內容返回給瀏覽器。

process_response()

簽名:process_response(request, response)

最主要的鉤子!

有兩個參數,request和response。request是請求內容,response是視圖函數返回的HttpResponse對象。該方法的返回值必須是一個HttpResponse對象,不能是None。

process_response()方法在視圖函數執行完畢以後執行,而且按配置順序的逆序執行。

process_view()

簽名:process_view(request, view_func, view_args, view_kwargs)

  • requestHttpRequest 對象。

  • view_func :真正的業務邏輯視圖函數(不是函數的字符串名稱)。

  • view_args :位置參數列表

  • view_kwargs :關鍵字參數字典

請務必牢記:process_view() 在Django調用真正的業務視圖以前被執行,而且以正序執行。當process_request()正常執行完畢後,會進入urlconf路由階段,並查找對應的視圖,在執行視圖函數以前,會先執行process_view() 中間件鉤子。

這個方法必須返回None 或者一個 HttpResponse 對象。若是返回的是None,Django將繼續處理當前請求,執行其它的 process_view() 中間件鉤子,最後執行對應的視圖。若是返回的是一個 HttpResponse 對象,Django不會調用業務視圖,而是執行響應中間件,並返回結果。

process_exception()

簽名:process_exception(request, exception)

  • requestHttpRequest對象

  • exception:視圖函數引起的具體異常對象

當一個視圖在執行過程當中引起了異常,Django將自動調用中間件的 process_exception()方法。 process_exception() 要麼返回一個 None ,要麼返回一個 HttpResponse 對象。若是返回的是HttpResponse對象 ,模板響應和響應中間件將被調用 ,不然進行正常的異常處理流程。

一樣的,此時也是以逆序的方式調用每一箇中間件的 process_exception方法,以短路的機制。

process_template_response()

簽名:process_template_response(request, response)

requestHttpRequest 對象

responseTemplateResponse 對象

process_template_response() 方法在業務視圖執行完畢後調用。

正常狀況下一個視圖執行完畢,會渲染一個模板,做爲響應返回給用戶。使用這個鉤子方法,你能夠從新處理渲染模板的過程,添加你須要的業務邏輯。

對於 process_template_response()方法,也是採用逆序的方式進行執行的。

鉤子方法執行流程

(注:全部圖片來自網絡,侵刪!)

一個理想狀態下的中間件執行過程,可能只有process_request()process_response()方法,其流程以下:

一旦任何一箇中間件返回了一個HttpResponse對象,馬上進入響應流程!要注意,未被執行的中間件,其響應鉤子方法也不會被執行,這是一個短路,或者說剝洋蔥的過程。

若是有process_view方法的介入,那麼會變成下面的樣子:

總的執行流程和機制以下圖所示:

仔細研究一下下面的執行流程,可以加深你對中間件的理解。

實例演示

介紹完了理論,下面經過實際的例子來演示一下。

要注意,之因此被稱爲傳統的方法,是由於這裏要導入一個未來會被廢棄的父類,也就是:

from django.utils.deprecation import MiddlewareMixin

deprecation是廢棄、貶低、折舊、反對的意思,也就是說,這個MiddlewareMixin類未來應該會被刪除!

咱們看一下MiddlewareMixin的源碼:

class MiddlewareMixin:
    def __init__(self, get_response=None):
        self.get_response = get_response
        super().__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__方法,經過hasattr的反射,尋找process_request等鉤子函數是否存在,若是存在就執行。它的本質和後面要介紹的Django官網提供的例子,也就是新的寫法是同樣的!

如今,假設咱們有一個app叫作midware,在其中建立一個middleware.py模塊,寫入下面的代碼:

from django.utils.deprecation import MiddlewareMixin


class Md1(MiddlewareMixin):

    def process_request(self,request):
        print("Md1處理請求")

    def process_response(self,request,response):
        print("Md1返回響應")
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):

        print("Md1在執行%s視圖前" %view_func.__name__)

    def process_exception(self,request,exception):
        print("Md1處理視圖異常...")



class Md2(MiddlewareMixin):

    def process_request(self,request):
        print("Md2處理請求")

    def process_response(self,request,response):
        print("Md2返回響應")
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):

        print("Md2在執行%s視圖前" %view_func.__name__)

    def process_exception(self,request,exception):
        print("Md2處理視圖異常...")

而後,咱們就能夠在setting.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',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'midware.middleware.Md1',
    'midware.middleware.Md2',
]

在midware/views.py中建立一個簡單的視圖:

from django.shortcuts import render, HttpResponse


def mid_test(request):
    print('執行視圖mid_test')
    # raise
    return HttpResponse('200,ok')

其中的raise能夠用來測試process_exception()鉤子。

編寫一條urlconf,用來測試視圖,好比:

from midware import views as mid_views

urlpatterns = [
    path('midtest/', mid_views.mid_test),
]

重啓服務器,訪問...../midtest/,能夠在控制檯看到以下的信息:

Md1處理請求
Md2處理請求
Md1在執行mid_test視圖前
Md2在執行mid_test視圖前
執行視圖mid_test
Md2返回響應
Md1返回響應

Django官方方法

在Django的官方文檔中(當前2.2),咱們能夠看到一種徹底不一樣的編寫方式。

這種編寫方式省去了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

或者寫成一個類(真.推薦形式),這個類的實例是可調用的,以下所示:

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

(是否是感受和前面的MiddlewareMixin類很像?)

Django 提供的 get_response 方法多是一個實際視圖(若是當前中間件是最後列出的中間件),或者是列表中的下一個中間件。咱們不須要知道或關心它究竟是什麼,它只是表明了下一步要進行的操做。

兩個注意事項:

  • Django僅使用 get_response 參數初始化中間件,所以不能爲 __init__() 添加其餘參數。

  • 與每次請求都會調用 __call__() 方法不一樣,當 Web 服務器啓動後,__init__() 只被調用一次

實例演示

咱們只須要把前面的Md1和Md2兩個中間件類修改爲下面的代碼就能夠了:

class Md1:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):

        print("Md1處理請求")

        response = self.get_response(request)

        print("Md1返回響應")

        return response

    def process_view(self, request, view_func, view_args, view_kwargs):

        print("Md1在執行%s視圖前" %view_func.__name__)

    def process_exception(self,request,exception):
        print("Md1處理視圖異常...")


class Md2:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print("Md2處理請求")

        response = self.get_response(request)

        print("Md2返回響應")

        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        print("Md2在執行%s視圖前" % view_func.__name__)

    def process_exception(self, request, exception):
        print("Md2處理視圖異常...")

能夠看到,咱們再也不須要繼承MiddlewareMixin類。

實際執行結果是同樣的。

應用實例一:IP攔截

若是咱們想限制某些IP對服務器的訪問,能夠在settings.py中添加一個BLACKLIST(全大寫)列表,將被限制的IP地址寫入其中。

而後,咱們就能夠編寫下面的中間件了:

from django.http import HttpResponseForbidden
from django.conf import settings

class BlackListMiddleware():

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):

        if request.META['REMOTE_ADDR'] in getattr(settings, "BLACKLIST", []):
            return HttpResponseForbidden('<h1>該IP地址被限制訪問!</h1>')

        response = self.get_response(request)

        return response

具體的中間件註冊、視圖、url就再也不贅述了。(歡迎你們訪問個人官網liujiangblog.com,更多精彩內容)

應用實例二:DEBUG頁面

網站上線正式運行後,咱們會將DEBUG改成 False,這樣更安全。可是發生服務器5xx系列錯誤時,管理員卻不能看到錯誤詳情,調試很不方便。有沒有辦法比較方便地解決這個問題呢?

  • 普通訪問者看到的是500錯誤頁面

  • 管理員看到的是錯誤詳情Debug頁面

利用中間件就能夠作到!代碼以下:

import sys
from django.views.debug import technical_500_response
from django.conf import settings


class DebugMiddleware():

    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,交給默認的流程處理
        if request.user.is_superuser or request.META.get('REMOTE_ADDR') in settings.ADMIN_IP:
            return technical_500_response(request, *sys.exc_info())

這裏經過if判斷,當前登陸的用戶是否超級管理員,或者當前用戶的IP地址是否在管理員IP地址列表中。符合二者之一,即判斷當前用戶有權限查看Debug頁面。

接下來註冊中間件,而後在測試視圖中添加一行raise。再修改settings.py,將Debug設爲False,提供ALLOWED_HOSTS = ["*"],設置好比ADMIN_IP = ['192.168.0.100'],而後啓動服務器0.0.0.0:8000,從不一樣的局域網IP來測試這個中間件。

正常狀況下,管理員應該看到相似下面的Debug頁面:

RuntimeError at /midtest/
No active exception to reraise
Request Method:	GET
Request URL:	http://192.168.0.100:8000/midtest/
Django Version:	2.0.7
Exception Type:	RuntimeError
Exception Value:	
No active exception to reraise
.....

而普通用戶只能看到:

A server error occurred. Please contact the administrator.

相關文章
相關標籤/搜索