Django專題-中介軟體
前戲
我們給檢視函式加裝飾器來判斷是使用者是否登入,把沒有登入的使用者請求跳轉到登入頁面。我們通過給幾個特定檢視函式加裝飾器實現了這個需求。但是以後新增的檢視函式可能也需要加上裝飾器,這樣是不是稍微有點繁瑣。
我們就可以通過中介軟體的方式來實現類似給所有請求都做相同操作的功能了。
中介軟體
中介軟體介紹
什麼是中介軟體?
官方的說法:中介軟體是一個用來處理Django的請求和響應的框架級別的鉤子。它是一個輕量、低級別的外掛系統,用於在全域性範圍內改變Django的輸入和輸出。每個中介軟體元件都負責做一些特定的功能。
但是由於其影響的是全域性,所以需要謹慎使用,使用不當會影響效能。
說的直白一點中介軟體是幫助我們在檢視函式執行之前和執行之後都可以做一些額外的操作,它本質上就是一個自定義類,類中定義了幾個方法,Django框架會在處理請求的特定的時間去執行這些方法。
我們一直都在使用中介軟體,只是沒有注意到而已,開啟Django專案的Settings.py檔案,看到下圖的MIDDLEWARE配置項。
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了,後來學會使用csrf_token之後就不再註釋這個中介軟體了。
那接下來就學習中介軟體中的方法以及這些方法什麼時候被執行。
自定義中介軟體
中介軟體可以定義五個方法,分別是:(主要的是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物件,則直接將該物件返回給使用者。
自定義一箇中間件示例
from django.utils.deprecation import MiddlewareMixin class MD1(MiddlewareMixin): def process_request(self, request): print("MD1裡面的 process_request") def process_response(self, request, response): print("MD1裡面的 process_response") return response
process_request
process_request有一個引數,就是request,這個request和檢視函式中的request是一樣的。
它的返回值可以是None也可以是HttpResponse物件。返回值是None的話,按正常流程繼續走,交給下一個中介軟體處理,如果是HttpResponse物件,Django將不執行檢視函式,而將響應物件返回給瀏覽器。
我們來看看多箇中間件時,Django是如何執行其中的process_request方法的。
from django.utils.deprecation import MiddlewareMixin class MD1(MiddlewareMixin): def process_request(self, request): print("MD1裡面的 process_request") class MD2(MiddlewareMixin): def process_request(self, request): print("MD2裡面的 process_request")
在settings.py的MIDDLEWARE配置項中註冊上述兩個自定義中介軟體:
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', 'middlewares.MD1', # 自定義中介軟體MD1 'middlewares.MD2' # 自定義中介軟體MD2 ]
此時,我們訪問一個檢視,會發現終端中列印如下內容:
MD1裡面的 process_request MD2裡面的 process_request
app01 中的 index檢視
把MD1和MD2的位置調換一下,再訪問一個檢視,會發現終端中列印的內容如下:
MD2裡面的 process_request MD1裡面的 process_request
app01 中的 index檢視
看結果我們知道:檢視函式還是最後執行的,MD2比MD1先執行自己的process_request方法。
在列印一下兩個自定義中介軟體中process_request方法中的request引數,會發現它們是同一個物件。
由此總結一下:
- 中介軟體的process_request方法是在執行檢視函式之前執行的。
- 當配置多箇中間件時,會按照MIDDLEWARE中的註冊順序,也就是列表的索引值,從前到後依次執行的。
- 不同中介軟體之間傳遞的request都是同一個物件
process_response
它有兩個引數,一個是request,一個是response,request就是上述例子中一樣的物件,response是檢視函式返回的HttpResponse物件。該方法的返回值也必須是HttpResponse物件。
給上述的M1和M2加上process_response方法:
from django.utils.deprecation import MiddlewareMixin class MD1(MiddlewareMixin): def process_request(self, request): print("MD1裡面的 process_request") def process_response(self, request, response): print("MD1裡面的 process_response") return response class MD2(MiddlewareMixin): def process_request(self, request): print("MD2裡面的 process_request") def process_response(self, request, response): print("MD2裡面的 process_response") return response
訪問一個檢視,看一下終端的輸出:
MD2裡面的 process_request MD1裡面的 process_request app01 中的 index檢視 MD1裡面的 process_response MD2裡面的 process_response
看結果可知:
process_response方法是在檢視函式之後執行的,並且順序是MD1比MD2先執行。(此時settings.py中 MD2比MD1先註冊)
多箇中間件中的process_response方法是按照MIDDLEWARE中的註冊順序倒序執行的,也就是說第一個中介軟體的process_request方法首先執行,而它的process_response方法最後執行,最後一箇中間件的process_request方法最後一個執行,它的process_response方法是最先執行。
process_view
process_view(self, request, view_func, view_args, view_kwargs)
該方法有四個引數
request是HttpRequest物件。
view_func是Django即將使用的檢視函式。 (它是實際的函式物件,而不是函式的名稱作為字串。)
view_args是將傳遞給檢視的位置引數的列表.
view_kwargs是將傳遞給檢視的關鍵字引數的字典。 view_args和view_kwargs都不包含第一個檢視引數(request)。
Django會在呼叫檢視函式之前呼叫process_view方法。
它應該返回None或一個HttpResponse物件。 如果返回None,Django將繼續處理這個請求,執行任何其他中介軟體的process_view方法,然後在執行相應的檢視。 如果它返回一個HttpResponse物件,Django不會呼叫適當的檢視函式。 它將執行中介軟體的process_response方法並將應用到該HttpResponse並返回結果。
給MD1和MD2新增process_view方法:
from django.utils.deprecation import MiddlewareMixin class MD1(MiddlewareMixin): def process_request(self, request): print("MD1裡面的 process_request") def process_response(self, request, response): print("MD1裡面的 process_response") return response def process_view(self, request, view_func, view_args, view_kwargs): print("-" * 80) print("MD1 中的process_view") print(view_func, view_func.__name__) class MD2(MiddlewareMixin): def process_request(self, request): print("MD2裡面的 process_request") def process_response(self, request, response): print("MD2裡面的 process_response") return response def process_view(self, request, view_func, view_args, view_kwargs): print("-" * 80) print("MD2 中的process_view") print(view_func, view_func.__name__)
訪問index檢視函式,看一下輸出結果:
MD2裡面的 process_request MD1裡面的 process_request -------------------------------------------------------------------------------- MD2 中的process_view <function index at 0x000001DE68317488> index -------------------------------------------------------------------------------- MD1 中的process_view <function index at 0x000001DE68317488> index app01 中的 index檢視 MD1裡面的 process_response MD2裡面的 process_response
process_view方法是在process_request之後,檢視函式之前執行的,執行順序按照MIDDLEWARE中的註冊順序從前到後順序執行的
process_exception
process_exception(self, request, exception)
該方法兩個引數:
一個HttpRequest物件
一個exception是檢視函式異常產生的Exception物件。
這個方法只有在檢視函式中出現異常了才執行,它返回的值可以是一個None也可以是一個HttpResponse物件。如果是HttpResponse物件,Django將呼叫模板和中介軟體中的process_response方法,並返回給瀏覽器,否則將預設處理異常。如果返回一個None,則交給下一個中介軟體的process_exception方法來處理異常。它的執行順序也是按照中介軟體註冊順序的倒序執行。
給MD1和MD2新增上這個方法:
from django.utils.deprecation import MiddlewareMixin class MD1(MiddlewareMixin): def process_request(self, request): print("MD1裡面的 process_request") def process_response(self, request, response): print("MD1裡面的 process_response") return response def process_view(self, request, view_func, view_args, view_kwargs): print("-" * 80) print("MD1 中的process_view") print(view_func, view_func.__name__) def process_exception(self, request, exception): print(exception) print("MD1 中的process_exception") class MD2(MiddlewareMixin): def process_request(self, request): print("MD2裡面的 process_request") def process_response(self, request, response): print("MD2裡面的 process_response") return response def process_view(self, request, view_func, view_args, view_kwargs): print("-" * 80) print("MD2 中的process_view") print(view_func, view_func.__name__) def process_exception(self, request, exception): print(exception) print("MD2 中的process_exception")
如果檢視函式中無異常,process_exception方法不執行。
想辦法,在檢視函式中丟擲一個異常:
def index(request): print("app01 中的 index檢視") raise ValueError("呵呵") return HttpResponse("O98K")
在MD1的process_exception中返回一個響應物件:
class MD1(MiddlewareMixin): def process_request(self, request): print("MD1裡面的 process_request") def process_response(self, request, response): print("MD1裡面的 process_response") return response def process_view(self, request, view_func, view_args, view_kwargs): print("-" * 80) print("MD1 中的process_view") print(view_func, view_func.__name__) def process_exception(self, request, exception): print(exception) print("MD1 中的process_exception") return HttpResponse(str(exception)) # 返回一個響應物件
看輸出結果:
MD2裡面的 process_request MD1裡面的 process_request -------------------------------------------------------------------------------- MD2 中的process_view <function index at 0x0000022C09727488> index -------------------------------------------------------------------------------- MD1 中的process_view <function index at 0x0000022C09727488> index app01 中的 index檢視 呵呵 MD1 中的process_exception MD1裡面的 process_response MD2裡面的 process_response
注意,這裡並沒有執行MD2的process_exception方法,因為MD1中的process_exception方法直接返回了一個響應物件。
process_template_response(用的比較少)
process_template_response(self, request, response)
它的引數,一個HttpRequest物件,response是TemplateResponse物件(由檢視函式或者中介軟體產生)。
process_template_response是在檢視函式執行完成後立即執行,但是它有一個前提條件,那就是檢視函式返回的物件有一個render()方法(或者表明該物件是一個TemplateResponse物件或等價方法)。
class MD1(MiddlewareMixin): def process_request(self, request): print("MD1裡面的 process_request") def process_response(self, request, response): print("MD1裡面的 process_response") return response def process_view(self, request, view_func, view_args, view_kwargs): print("-" * 80) print("MD1 中的process_view") print(view_func, view_func.__name__) def process_exception(self, request, exception): print(exception) print("MD1 中的process_exception") return HttpResponse(str(exception)) def process_template_response(self, request, response): print("MD1 中的process_template_response") return response class MD2(MiddlewareMixin): def process_request(self, request): print("MD2裡面的 process_request") def process_response(self, request, response): print("MD2裡面的 process_response") return response def process_view(self, request, view_func, view_args, view_kwargs): print("-" * 80) print("MD2 中的process_view") print(view_func, view_func.__name__) def process_exception(self, request, exception): print(exception) print("MD2 中的process_exception") def process_template_response(self, request, response): print("MD2 中的process_template_response") return response
views.py中:
def index(request): print("app01 中的 index檢視") def render(): print("in index/render") return HttpResponse("O98K") rep = HttpResponse("OK") rep.render = render return rep
訪問index檢視,終端輸出的結果:
MD2裡面的 process_request MD1裡面的 process_request -------------------------------------------------------------------------------- MD2 中的process_view <function index at 0x000001C111B97488> index -------------------------------------------------------------------------------- MD1 中的process_view <function index at 0x000001C111B97488> index app01 中的 index檢視 MD1 中的process_template_response MD2 中的process_template_response in index/render MD1裡面的 process_response MD2裡面的 process_response
從結果看出:
檢視函式執行完之後,立即執行了中介軟體的process_template_response方法,順序是倒序,先執行MD1的,在執行MD2的,接著執行了檢視函式返回的HttpResponse物件的render方法,返回了一個新的HttpResponse物件,接著執行中介軟體的process_response方法。
中介軟體的執行流程
上一部分,我們瞭解了中介軟體中的5個方法,它們的引數、返回值以及什麼時候執行,現在總結一下中介軟體的執行流程。
請求到達中介軟體之後,先按照正序執行每個註冊中介軟體的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方法開始倒序執行。
process_template_response和process_exception兩個方法的觸發是有條件的,執行順序也是倒序。總結所有的執行流程如下:
中介軟體版登入驗證
中介軟體版的登入驗證需要依靠session,所以資料庫中要有django_session表。
urls.py
from django.conf.urls import url from django.contrib import admin from app01 import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^login/$', views.login, name='login'), url(r'^index/$', views.index, name='index'), url(r'^home/$', views.home, name='home'), ]
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 == "alex" and pwd == "alex3714": # 設定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' %}" method="post"> {% csrf_token %} <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
from django.utils.deprecation import MiddlewareMixin class AuthMD(MiddlewareMixin): white_list = ['/login/', ] # 白名單 black_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.black_list: return HttpResponse('This is an illegal URL') # 白名單的網址或者登陸使用者不做限制 elif next_url in self.white_list or request.session.get("user"): return 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', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'middlewares.AuthMD' ]
AuthMD中介軟體註冊後,所有的請求都要走AuthMD的process_request方法。
如果URL在黑名單中,則返回This is an illegal URL的字串;
訪問的URL在白名單內或者session中有user使用者名稱,則不做阻攔走正常流程;
正常的URL但是需要登入後訪問,讓瀏覽器跳轉到登入頁面。
注:AuthMD中介軟體中需要session,所以AuthMD註冊的位置要在session中間的下方。