1. 程式人生 > >Django學習筆記10

Django學習筆記10

中介軟體

中介軟體介紹

什麼是中介軟體?

官方的說法:中介軟體是一個用來處理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物件,則直接將該物件返回給使用者。

自定義一箇中間件示例(自定義之後要在MIDDLEWARE中註冊才有用)

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")
        pass

  在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引數,會發現它們是同一個物件。

由此總結一下:

  1. 中介軟體的process_request方法是在執行檢視函式之前執行的。
  2. 當配置多箇中間件時,會按照MIDDLEWARE中的註冊順序,也就是列表的索引值,從前到後依次執行的。
  3. 不同中介軟體之間傳遞的request都是同一個物件

process_response

執行順序:
按照註冊順序的倒序(在settings.py裡面設定中 從下到上的順序)
何時執行:
請求有響應的時候
返回值:
必須返回一個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")
        pass

    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_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")
        pass

    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方法來處理異常。它的執行順序也是按照中介軟體註冊順序的倒序執行。

執行順序:
按照註冊順序的倒序(在settings.py裡面設定中 從下到上的順序)
何時執行:
檢視函式中丟擲異常的時候才執行
返回值:
返回None,繼續執行後續中介軟體的process_exception
返回response

process_template_response(用的比較少)

process_template_response(self, request, response)
執行順序:
按照註冊順序的倒序(在settings.py裡面設定中 從下到上的順序)
何時執行:
檢視函式執行完,在執行檢視函式返回的響應物件的render方法之前
返回值:
返回None,繼續執行後續中介軟體的process_exception
返回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 app01 import views

urlpatterns = [
    url(r'^index/$', views.index),
    url(r'^login/$', views.login, name='login'),
]

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 == "Q1mi" and pwd == "123456":
            # 設定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' %}">
    <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

class AuthMD(MiddlewareMixin):
    white_list = ['/login/', ]  # 白名單
    balck_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.white_list or request.session.get("user"):
            return
        elif next_url in self.balck_list:
            return HttpResponse('This is an illegal URL')
        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',
    'middlewares.AuthMD',
]

AuthMD中介軟體註冊後,所有的請求都要走AuthMD的process_request方法。

訪問的URL在白名單內或者session中有user使用者名稱,則不做阻攔走正常流程;

如果URL在黑名單中,則返回This is an illegal URL的字串;

正常的URL但是需要登入後訪問,讓瀏覽器跳轉到登入頁面。

注:AuthMD中介軟體中需要session,所以AuthMD註冊的位置要在session中間的下方。

Django請求流程圖

Django 的 CSRF 保護機制

  • 每次初始化一個專案時都能看到 django.middleware.csrf.CsrfViewMiddleware 這個中介軟體
  • 每次在模板裡寫 form 時都知道要加一個 {% csrf_token %} tag
  • 每次發 ajax POST 請求,都需要加一個 X_CSRFTOKEN 的 header

什麼是 CSRF 

CSRF, Cross Site Request Forgery, 跨站點偽造請求。

舉例來講,當你看小姐姐照片的同時,登陸了銀行網站,然後你銀行賬戶還沒有退出,這時,你又回去看小姐姐,點選了小姐姐的圖片,但是,小姐姐的圖片上有個連結,正好是一個post請求發給你登陸的銀行網頁,請求的內容就是給小姐姐轉賬,平時情況下,你沒登陸,轉賬不可能成功(後天驗證不通過),但是,你賬戶是你自己登陸的(有cookie了),連結是你自己點的,就這樣,你的錢就到小姐姐那裡去了。

正經一點的話就是:

別的某個惡意的網站上有一個指向你的網站的連結,如果某個使用者已經登入到你的網站上了,那麼當這個使用者點選這個惡意網站上的那個連結時,就會向你的網站發來一個請求,你的網站會以為這個請求是使用者自己發來的,其實呢,這個請求是那個惡意網站偽造的。

要完成一次CSRF攻擊,受害者必須依次完成兩個步驟:

  1.登入受信任網站A,並在本地生成Cookie。

  2.在不登出A的情況下,訪問危險網站B。

Django 提供的 CSRF 防護機制

django 第一次響應來自某個客戶端的請求時,會在伺服器端隨機生成一個 token,把這個 token 放在 cookie 裡。然後每次 POST 請求都會帶上這個 token,這樣就能避免被 CSRF 攻擊。

簡單的說就是在form表單里加個csrftoken 欄位,因為攻擊者不能獲得第三方的Cookie(理論上),也不知道伺服器上生成的欄位值,所以表單中的資料也就構造失敗了

  1. 在返回的 HTTP 響應的 cookie 裡,django 會為你新增一個 csrftoken 欄位,其值為一個自動生成的 token
  2. 在所有的 POST 表單時,必須包含一個 csrfmiddlewaretoken 欄位 (只需要在模板里加一個 tag, django 就會自動幫你生成,見下面)
  3. 在處理 POST 請求之前,django 會驗證這個請求的 cookie 裡的 csrftoken 欄位的值和提交的表單裡的 csrfmiddlewaretoken 欄位的值是否一樣。如果一樣,則表明這是一個合法的請求,否則,這個請求可能是來自於別人的 csrf 攻擊,返回 403 Forbidden.
  4. 在所有 ajax POST 請求裡,新增一個 X-CSRFTOKEN header,其值為 cookie 裡的 csrftoken 的值

Django 裡如何使用 CSRF 防護

  • 首先,最基本的原則是:GET 請求不要用有副作用。也就是說任何處理 GET 請求的程式碼對資源的訪問都一定要是“只讀“的。
  • 要啟用 django.middleware.csrf.CsrfViewMiddleware 這個中介軟體
  • 再次,在所有的 POST 表單元素時,需要加上一個 {% csrf_token %} tag
  • 在渲染模組時,使用 RequestContext。RequestContext 會處理 csrf_token 這個 tag,  從而自動為表單新增一個名為 csrfmiddlewaretoken 的 input
  • 每次初始化一個專案時都能看到 django.middleware.csrf.CsrfViewMiddleware 這個中介軟體
  • 每次在模板裡寫 form 時都知道要加一個 {% csrf_token %} tag
  • 每次發 ajax POST 請求,都需要加一個 X_CSRFTOKEN 的 header

什麼是 CSRF 

CSRF, Cross Site Request Forgery, 跨站點偽造請求。

舉例來講,當你看小姐姐照片的同時,登陸了銀行網站,然後你銀行賬戶還沒有退出,這時,你又回去看小姐姐,點選了小姐姐的圖片,但是,小姐姐的圖片上有個連結,正好是一個post請求發給你登陸的銀行網頁,請求的內容就是給小姐姐轉賬,平時情況下,你沒登陸,轉賬不可能成功(後天驗證不通過),但是,你賬戶是你自己登陸的(有cookie了),連結是你自己點的,就這樣,你的錢就到小姐姐那裡去了。

正經一點的話就是:

別的某個惡意的網站上有一個指向你的網站的連結,如果某個使用者已經登入到你的網站上了,那麼當這個使用者點選這個惡意網站上的那個連結時,就會向你的網站發來一個請求,你的網站會以為這個請求是使用者自己發來的,其實呢,這個請求是那個惡意網站偽造的。

要完成一次CSRF攻擊,受害者必須依次完成兩個步驟:

  1.登入受信任網站A,並在本地生成Cookie。

  2.在不登出A的情況下,訪問危險網站B。

Django 提供的 CSRF 防護機制

django 第一次響應來自某個客戶端的請求時,會在伺服器端隨機生成一個 token,把這個 token 放在 cookie 裡。然後每次 POST 請求都會帶上這個 token,這樣就能避免被 CSRF 攻擊。

簡單的說就是在form表單里加個csrftoken 欄位,因為攻擊者不能獲得第三方的Cookie(理論上),也不知道伺服器上生成的欄位值,所以表單中的資料也就構造失敗了

  1. 在返回的 HTTP 響應的 cookie 裡,django 會為你新增一個 csrftoken 欄位,其值為一個自動生成的 token
  2. 在所有的 POST 表單時,必須包含一個 csrfmiddlewaretoken 欄位 (只需要在模板里加一個 tag, django 就會自動幫你生成,見下面)
  3. 在處理 POST 請求之前,django 會驗證這個請求的 cookie 裡的 csrftoken 欄位的值和提交的表單裡的 csrfmiddlewaretoken 欄位的值是否一樣。如果一樣,則表明這是一個合法的請求,否則,這個請求可能是來自於別人的 csrf 攻擊,返回 403 Forbidden.
  4. 在所有 ajax POST 請求裡,新增一個 X-CSRFTOKEN header,其值為 cookie 裡的 csrftoken 的值

Django 裡如何使用 CSRF 防護

  • 首先,最基本的原則是:GET 請求不要用有副作用。也就是說任何處理 GET 請求的程式碼對資源的訪問都一定要是“只讀“的。
  • 要啟用 django.middleware.csrf.CsrfViewMiddleware 這個中介軟體
  • 再次,在所有的 POST 表單元素時,需要加上一個 {% csrf_token %} tag
  • 在渲染模組時,使用 RequestContext。RequestContext 會處理 csrf_token 這個 tag,  從而自動為表單新增一個名為 csrfmiddlewaretoken 的 input