之前我們已經實現了使用者必須登入才能投票的限制,但是一個新的問題來了。如果我們的應用中有很多功能都需要使用者先登入才能執行,例如將前面匯出Excel報表和檢視統計圖表的功能都做了必須登入才能訪問的限制,那麼我們是不是需要在每個檢視函式中新增程式碼來檢查session中是否包含userid
的程式碼呢?答案是否定的,如果這樣做了,我們的檢視函式中必然會充斥著大量的重複程式碼。程式設計大師Martin Fowler曾經說過:程式碼有很多種壞味道,重複是最壞的一種。在Python程式中,我們可以透過裝飾器來為函式提供額外的能力;在Django專案中,我們可以把類似於驗證使用者是否登入這樣的重複性程式碼放到中介軟體中。
中介軟體是安插在Web應用請求和響應過程之間的元件,它在整個Web應用中扮演了攔截過濾器的角色,透過中介軟體可以攔截請求和響應,並對請求和響應進行過濾(簡單的說就是執行額外的處理)。通常,一箇中間件元件只專注於完成一件特定的事,例如:Django框架透過SessionMiddleware
中介軟體實現了對session的支援,又透過AuthenticationMiddleware
中介軟體實現了基於session的請求認證。透過把多箇中間件組合在一起,我們可以完成更為複雜的任務,Django框架就是這麼做的。
Django專案的配置檔案中就包含了對中介軟體的配置,程式碼如下所示。
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',
]
我們稍微為大家解釋一下這些中介軟體的作用:
CommonMiddleware
- 基礎設定中介軟體,可以處理以下一些配置引數。- DISALLOWED_USER_AGENTS - 不被允許的使用者代理(瀏覽器)
- APPEND_SLASH - 是否追加
/
- USE_ETAG - 瀏覽器快取相關
SecurityMiddleware
- 安全相關中介軟體,可以處理和安全相關的配置項。- SECURE_HSTS_SECONDS - 強制使用HTTPS的時間
- SECURE_HSTS_INCLUDE_SUBDOMAINS - HTTPS是否覆蓋子域名
- SECURE_CONTENT_TYPE_NOSNIFF - 是否允許瀏覽器推斷內容型別
- SECURE_BROWSER_XSS_FILTER - 是否啟用跨站指令碼攻擊過濾器
- SECURE_SSL_REDIRECT - 是否重定向到HTTPS連線
- SECURE_REDIRECT_EXEMPT - 免除重定向到HTTPS
SessionMiddleware
- 會話中介軟體。CsrfViewMiddleware
- 透過生成令牌,防範跨請求份偽的造中介軟體。XFrameOptionsMiddleware
- 透過設定請求頭引數,防範點選劫持攻擊的中介軟體。
在請求的過程中,上面的中介軟體會按照書寫的順序從上到下執行,然後是URL解析,最後請求才會來到檢視函式;在響應的過程中,上面的中介軟體會按照書寫的順序從下到上執行,與請求時中介軟體執行的順序正好相反。
Django中的中介軟體有兩種實現方式:基於類的實現方式和基於函式的實現方式,後者更接近於裝飾器的寫法。裝飾器實際上是代理模式的應用,將橫切關注功能(與正常業務邏輯沒有必然聯絡的功能,例如:身份認證、日誌記錄、編碼轉換之類的功能)置於代理中,由代理物件來完成被代理物件的行為並新增額外的功能。中介軟體對使用者請求和響應進行攔截過濾並增加額外的處理,在這一點上它跟裝飾器是完全一致的,所以基於函式的寫法來實現中介軟體就跟裝飾器的寫法幾乎一模一樣。下面我們用自定義的中介軟體來實現使用者登入驗證的功能。
"""
middlewares.py
"""
from django.http import JsonResponse
from django.shortcuts import redirect
# 需要登入才能訪問的資源路徑
LOGIN_REQUIRED_URLS = {'/praise/', '/criticize/', '/excel/', '/teachers_data/'}
def check_login_middleware(get_resp):
def wrapper(request, *args, **kwargs):
# 請求的資源路徑在上面的集合中
if request.path in LOGIN_REQUIRED_URLS:
# 會話中包含userid則視為已經登入
if 'userid' not in request.session:
# 判斷是不是Ajax請求
if request.is_ajax():
# Ajax請求返回JSON資料提示使用者登入
return JsonResponse({'code': 10003, 'hint': '請先登入'})
else:
backurl = request.get_full_path()
# 非Ajax請求直接重定向到登入頁
return redirect(f'/login/?backurl={backurl}')
return get_resp(request, *args, **kwargs)
return wrapper
當然,我們也可以定義一個類來充當裝飾器,如果類中有__call__
魔術方法,這個類的物件就像函式一樣可呼叫,所以下面是另一種實現中介軟體的方式,道理跟上面的程式碼完全一樣。
還有一種基於類實現中介軟體的方式,這種方式在較新版本的Django中已經不推薦使用了,但是大家接觸到的程式碼中,仍然有可能遇到這種寫法,大致的程式碼如下所示。
from django.utils.deprecation import MiddlewareMixin
class MyMiddleware(MiddlewareMixin):
def process_request(self, request):
pass
def process_view(self, request, view_func, view_args, view_kwargs):
pass
def process_template_response(self, request, response):
pass
def process_response(self, request, response):
pass
def process_exception(self, request, exception):
pass
上面類中的五個方法都是中介軟體的鉤子函式,分別在收到使用者請求、進入檢視函式之前、渲染模板、返回響應和出現異常的時候被回撥。當然,寫不寫這些方法是根據中介軟體的需求來確定的,並不是所有的場景都需要重寫五個方法,下面的圖相信能夠幫助大家理解這種寫法。
寫好中介軟體程式碼後,需要修改配置檔案來啟用中介軟體使其生效。
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',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'vote.middlewares.check_login_middleware',
]
注意上面這個中介軟體列表中元素的順序,當收到來自使用者的請求時,中介軟體按照從上到下的順序依次執行,這行完這些中介軟體以後,請求才會最終到達檢視函式。當然,在這個過程中,使用者的請求可以被攔截,就像上面我們自定義的中介軟體那樣,如果使用者在沒有登入的情況下訪問了受保護的資源,中介軟體會將請求直接重定向到登入頁,後面的中介軟體和檢視函式將不再執行。在響應使用者請求的過程中,上面的中介軟體會按照從下到上的順序依次執行,這樣的話我們還可以對響應做進一步的處理。
中介軟體執行的順序是非常重要的,對於有依賴關係的中介軟體必須保證被依賴的中介軟體要置於依賴它的中介軟體的前面,就好比我們剛才自定義的中介軟體要放到SessionMiddleware
的後面,因為我們要依賴這個中介軟體為請求繫結的session
物件才能判定使用者是否登入。