Django 中間件介紹及修改請求參數
本文將主要介紹:Django 中間件介紹、 Django 中間件使用、Django 中間件中增加請求參數
1. 業務場景
在 API 接口中,在請求頭中增加了 token 驗證,其中,token 是 JWT 類型,也就是說,不僅需要驗證 token 的有效性和合法性,同時也要解析 token 中所攜帶的數據信息,最后還需將解析出來的數據,加入到 HttpRequest 中,供接口使用
2. Django 中間件介紹
Django 中間件是修改 Django request 或者 response 對象的鈎子,可以理解為是介於 HttpRequest 與 HttpResponse 處理之間的一道處理過程。瀏覽器從請求到響應的過程中,Django 需要通過很多中間件來處理,可以看如下圖所示:
2.1 Django 中間件作用
- 修改請求,即傳送到 view 中的 HttpRequest 對象
- 修改響應,即 view 返回的 HttpResponse 對象
2.2 中間件組件配置
在 settings.py 文件的 MIDDLEWARE 選項列表中,配置中的每個字符串選項都是一個類,也就是一個中間件
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',
]
2.3 自定義中間件
2.3.1 方法一:繼承 MiddlewareMixin(官方不建議)
中間件可以定義四個方法,分別是:
process_request(self,request)
process_view(self, request, view_func, view_args, view_kwargs)
process_exception(self, request, exception)
process_response(self, request, response)
自定義中間的步驟:
在 app 目錄下新建一個 py 文件,名字自定義,並在該 py 文件中導入 MiddlewareMixin:
from django.utils.deprecation import MiddlewareMixin
自定義的中間件類,要繼承父類 MiddlewareMixin:
class MD1(MiddlewareMixin):
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',
'app01.middlewares.MD1',
]
中間件類的方法:
- process_request
- process_response
- process_view
- process_exception
- process_template_response()
process_request(self, request)
-
參數:
- request,這個 request 和視圖函數中的 request 是一樣的
-
返回值:
- None 的話,按正常流程繼續走,交給下一個中間件處理
- 返回值是 HttpResponse 對象,Django 將不執行后續視圖函數之前執行的方法以及視圖函數,直接以該中間件為起點,倒序執行中間件,且執行的是視圖函數之后執行的方法
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import render, HttpResponse
class MD1(MiddlewareMixin):
def process_request(self, request):
print("md1 process_request 方法。", id(request))
注:當配置多個中間件時,會按照 MIDDLEWARE中 的注冊順序,也就是列表的索引值,順序執行
process_response(self,request, response)
- 參數:
- request 是請求對象
- response 是視圖函數返回的 HttpResponse 對象,該方法必須要有返回值,且必須是 response
- 執行順序:
- process_response 方法是在視圖函數之后執行的
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import render, HttpResponse
class MD1(MiddlewareMixin):
def process_request(self, request):
print("md1 process_request 方法。", id(request)) #在視圖之前執行
def process_response(self,request, response):
print("md1 process_response 方法!", id(request))
return response
注:當配置多個中間件時,會按照 MIDDLEWARE 中的注冊順序,也就是列表的索引值,倒序執行
從下圖看,正常的情況下按照綠色的路線進行執行,假設 中間件1 有返回值,則按照紅色的路線走,直接執行該類下的 process_response 方法返回,后面的其他中間件就不會執行
process_view(self,request, view_func, view_args, view_kwargs)
- 參數:
- request 是 HttpRequest 對象
- view_func 是 Django 即將使用的視圖函數
- view_args 是將傳遞給視圖的位置參數的列表
- view_kwargs 是將傳遞給視圖的關鍵字參數的字典
- 返回值:
- 是 None 的話,按正常流程繼續走,交給下一個中間件處理
- 返回值是 HttpResponse 對象,Django 將不執行后續視圖函數之前執行的方法以及視圖函數,直接以該中間件為起點,倒序執行中間件,且執行的是視圖函數之后執行的方法
- 是 view_func(request),Django 將不執行后續視圖函數之前執行的方法,提前執行視圖函數,然后再倒序執行視圖函數之后執行的方法
- 執行順序:
- process_view 方法是在視圖函數之前,process_request 方法之后執行的
class MD1(MiddlewareMixin):
def process_request(self, request):
print("md1 process_request 方法。", id(request)) # 在視圖之前執行
def process_response(self,request, response): # 基於請求響應
print("md1 process_response 方法!", id(request)) # 在視圖之后
return response
def process_view(self,request, view_func, view_args, view_kwargs):
print("md1 process_view 方法!") # 在視圖之前執行 順序執行
#return view_func(request)
注:
- view_args 和 view_kwargs 都不包含第一個視圖參數(request)
從下圖看,當最后一個中間件的 process_request 到達路由關系映射之后,返回到第一個中間件 process_view,然后依次往下,到達視圖函數
process_exception(self, request, exception)
- 參數:
- request 是 HttpRequest 對象
- exception 是視圖函數異常產生的 Exception 對象
- 返回值:
- 返回值是 None,頁面會報 500 狀態碼錯誤,視圖函數不會執行
- 是 HttpResponse 對象,頁面不會報錯,返回狀態碼為 200
- 執行順序:
- 在視圖函數之后,在 process_response 方法之前執行
- process_exception 方法倒序執行,然后再倒序執行 process_response 方法
- process_exception 方法只有在視圖函數中出現異常了才執行,按照 settings 的注冊倒序執行
- 視圖函數不執行,該中間件后續的 process_exception 方法也不執行,直接從最后一個中間件的 process_response 方法倒序開始執行
- 若是 process_view 方法返回視圖函數,提前執行了視圖函數,且視圖函數報錯,則無論 process_exception 方法的返回值是什么,頁面都會報錯, 且視圖函數和 process_exception 方法都不執行
class MD1(MiddlewareMixin):
def process_request(self, request):
print("md1 process_request 方法。", id(request)) # 在視圖之前執行
def process_response(self,request, response): # 基於請求響應
print("md1 process_response 方法!", id(request)) # 在視圖之后
return response
def process_view(self,request, view_func, view_args, view_kwargs):
print("md1 process_view 方法!") # 在視圖之前執行 順序執行
#return view_func(request)
def process_exception(self, request, exception): # 引發錯誤 才會觸發這個方法
print("md1 process_exception 方法!")
# return HttpResponse(exception) # 返回錯誤信息
2.3.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
2.3.3 方法三:類方式(推薦)
寫成一個類,這個類的實例是可調用的
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
def process_view(self, request, view_func, view_args, view_kwargs):
pass
def process_exception(self,request,exception):
pass
Django 提供的 get_response
方法可能是一個實際視圖(如果當前中間件是最后列出的中間件),或者是列表中的下一個中間件。我們不需要知道或關心它到底是什么,它只是代表了下一步要進行的操作
注意事項:
-
Django 僅使用
get_response
參數初始化中間件,因此不能為__init__()
添加其他參數 -
與每次請求都會調用
__call__()
方法不同,當 Web 服務器啟動后,__init__()
只被調用一次
3. Django 中間件中增加請求參數
主要想法,獲取請求頭里面的 token(也可以放在參數里),進行解析和驗證,將解析得到的數據,放到請求參數HttpRequest 里,供視圖使用
from django.http import HttpRequest, HttpResponse
from django.conf import settings
from jwttoken import JwtToken
class TokenVerifyMiddleWare():
""" this is token verification middleware
1. if you want to exempt token verification, you could set the path in the settings.EXEMPT_TOKEN_VERIFY_PATH
"""
_exempt_token_verify_path = settings.EXEMPT_TOKEN_VERIFY_PATH
_request_params_map_items = [
dict(request_method="GET", content_types=["text/plain", ''], request_params_key="GET", mutable_key="GET"),
dict(request_method="POST", content_types=["text/plain", 'application/x-www-form-urlencoded', "multipart/form-data"], request_params_key="POST", mutable_key="POST"),
dict(request_method="POST", content_types=["application/json"], request_params_key="body", mutable_key="_body")]
def __init__(self, get_response: callable) -> None:
self.get_response = get_response
def _is_exempt_verify_path(self, request: HttpRequest) -> bool:
"判斷路徑是否需要 token 驗證"
path_verify_result = False
request_path = request.path
if request_path in self._exempt_token_verify_path or request_path.startswith(self._exempt_file_download_path):
path_verify_result = True
return path_verify_result
def __call__(self, request: HttpRequest) -> HttpResponse:
token_verify_status = True
if not self._is_exempt_verify_path(request):
token_verify_status, payload_data = self._token_verify(request)
self._attach_token_payload(request, payload_data)
response = self.get_response(request) if token_verify_status else HttpResponse(MyTools.to_pretty_dict_str(payload_data))
return response
def _token_verify(self, request: HttpRequest) -> None:
"進行 token 驗證"
token = request.META.get('HTTP_HWTOKEN')
verify_status, payload_data = JwtToken.parse_token(token)
return verify_status, payload_data
def _attach_token_payload(self, request: HttpRequest, payload_data: dict) -> None:
"將 token 解析出來的數據,添加進 request"
request_key_info = self._get_request_params_map_infos(request)
request_params_key = request_key_info["request_params_key"]
mutable_key = request_key_info["mutable_key"]
raw_request_params = getattr(request, request_params_key)
is_bytes_format = isinstance(raw_request_params, bytes)
request_params = raw_request_params.copy() if not is_bytes_format else json.loads(raw_request_params.decode())
request_params["personInfo"] = payload_data
uniform_request_params = request_params if not is_bytes_format else json.dumps(request_params, ensure_ascii=False).encode()
setattr(request, mutable_key, uniform_request_params)
def _get_request_params_map_infos(self, request: HttpRequest) -> dict:
"根據 request 映射到對應的 params item"
request_method = request.method
content_type = request.content_type
ret = dict(request_params_key=request_method, mutable_key=request_method)
for request_params_item in self._request_params_map_items:
support_request_method = request_params_item["request_method"]
support_content_types = request_params_item["content_types"]
if support_request_method == request_method and content_type in support_content_types:
ret = request_params_item
break
return ret
主要知識點:
-
獲取請求參數並修改
# 獲取請求參數,這個參數是不可以修改的 immurequest_params = getattr(request, request_method) # 復制一份請求參數,變為可以修改 mutable_request_params = immurequest_params.copy() # 修改請求參數 mutable_request_params['personInfo'] = payload_data # 將修改后的請求參數放到請求頭 setattr(request, request_method, mutable_request_params) # 沒有辦法直接對 body 進行賦值 setattr(request, "_body", mutable_request_params)
-
請求中其他參數
- request.scheme : 請求協議
- request.body : 請求主體(get沒有請求主體)
- request.path : 請求路徑(具體資源路徑)
- request.get_host() : 請求的主機地址 / 域名
- request.method : 獲取請求方法
- request.GET : 封裝了get請求方式所提交的數據(字典)
- request.POST : 封裝了post請求方式所提交的數據(字典)
- request.COOKIES : 封裝了 cookies 中的所有數據
- request.META : 封裝了請求的源數據
-
如何獲取請求頭數據
- request.META.get("header key") 用於獲取 header 的信息
- 注意的是 header key 必須增加前綴 HTTP,同時大寫,例如你的 key 為
username
,那么應該寫成:request.META.get("HTTP_USERNAME")
- 另外就是當你的 header key 中帶有中橫線,那么自動會被轉成下划線,例如
my-user
的寫成:request.META.get("HTTP_MY_USER")