讓你的Django應用變DRY的幾個最佳實踐
- 2019 年 12 月 1 日
- 筆記
目前在Python的Web框架中被應用最廣泛的就是Django和Django REST Framework. 這兩種框架都提供了非常健壯的功能,能滿足Web開發的各個方面。DRY是Don't-Repeat-Yourself的縮寫,是一種程式碼編寫的原則,即不要重複自己的工作。我個人有些程式碼潔癖,凡是發現我需要複製粘貼程式碼的地方,就想著能怎樣去除重複的工作。在日常的開發中也總結出了一些個人的實踐,分享給大家。
總的來說,要使得你的應用很DRY,要遵循以下兩個原則:
- 全局都應用的變更,收攏到一個地方配置
- 有少數與其他不一樣的行為,將多數行為定義為全局行為,將少數行為分別配置,並儘可能簡化配置方法。
Django和Django REST framework(後簡稱DRF)提供了海量的全局配置、局部配置,來實現上述思想,但配置項太多了,有時人們往往不知道該如何利用。
一、用戶鑒權
1. Django的配置AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS
控制了應用根據傳入的參數校驗用戶是否屬於合法用戶(用戶名是否存在?密碼是否正確?)。使用時通過django.contrib.auth.authenticate
函數,傳入想要的參數,該函數會自動選擇對應的後端進行用戶校驗,常用的校驗方式有資料庫校驗、配置文件校驗、LDAP校驗等等。如果你想接入第三方登錄,OAuth登錄,都應該自定義一個Backend,無需繼承任何基類,只需實現一個authenticate方法,該方法參數與django.contrib.auth.authenticate
的傳入參數相同,返回一個用戶對象,然後將這個Backend添加到AUTHENTICATION_BACKENDS
就可以了。
注意:在使用到用戶模型的時候,要使用django.contrib.auth.get_user_model()
而不是導入具體的model類,這樣可以方便用AUTH_USER_MODEL
配置去改變用戶模型。
Python
class PowerOAuthBackend: """請求Power單點登錄後跳轉的驗證""" def authenticate(self, request, user=None, password=None): if check_user_password(user, password): # 返回用戶對象 return get_user_model().get(username=user) else: # 用戶名密碼錯誤 403 raise PermissionDenied() def get_user(self, user_id): # 若通過瀏覽器訪問則需要定義次方法,獲取已登錄的用戶對象 # 若只有RESTful調用則跳過 return get_user_model().objects.get(staff_id=user_id) # 登錄 def login_view(request): username = request.POST.get('user') password = request.POST.get('password') user = authenticate(user=username, password=password) # 將用戶存入會話 login(request, user) return redirect('/')
2. DRF的配置 DEFAULT_AUTHENTICATION_CLASSES
DEFAULT_AUTHENTICATION_CLASSES
,以及針對每個APIView
配置的authentication_classes
,是對RESTful請求的身份驗證,通過分析請求帶的身份資訊判斷來源方的身份,一般有以下幾種方式:
- 會話鑒權(登錄態)
- BasicAuth鑒權
- Token鑒權
這些類都包含在rest_framework.authentication
模組中。如果你要通過智慧網關轉發後端請求,則需要寫一個Authentication類,繼承自rest_framework.authentication.BaseAuthentication
類,其中有兩個比較重要的方法,函數簽名及說明如下:
Python
class MyAuthentication(BaseAuthentication): def authenticate(self, request): # 若鑒權成功,則返回一個(user, auth)的元組 return user, auth # 否則,若想交給後面的authentication處理,則返回None return None # 否則拋出401錯誤 raise rest_framework.exceptions.AuthenticationFailed() def authenticate_header(self, request): # DRF會選擇第一順位的Authentication的此方法返回的結果作為WWW-Authentication頭 # 如果返回為空則會將401錯誤轉換成403錯誤 return 'OMS'
3. DRF的DEFAULT_PERMISSION_CLASSES
如果說Authentication是判斷「你是誰」,那麼Authorization就是判斷「你能做什麼」,就好比你進入公司大樓需要用工卡(Authentication),但你有了工卡也不能隨便去總裁辦公室(Authorization)。在DRF中完成Authorization工作的就是DEFAULT_PERMISSION_CLASSES
配置項,以及針對每個APIView
配置的permission_classes
,他是用來精確控制請求放對某一資源有無許可權。在RESTful規範中,無鑒權資訊是401錯誤而無許可權是403錯誤。在DRF的官方文檔中有詳細例子這裡就不再贅述。
二、自定義響應體
很多時候(如前端框架、開發SDK)對響應體的格式是有要求的,我看到大多數的實現只是用一個格式化的類去填充響應資訊,但這種方法有兩個缺點:
- 每次需要人為構造響應
- 無法適用於DRF的
ModelViewSet
,因為它自帶的方法的響應是默認的,如果要挨個重載就無法利用到ModelViewSet
的懶人特性
所以我們需要將這種格式自定義收攏到一處,做到使用時無感知,響應自動形成期望的格式。要達成這種效果,大致有兩種途徑:
- 寫自定義中間件,修改響應格式
- 寫自定義renderer
這裡第一種途徑有幾處劣勢:
- 在中間件處理時
rest_framework.response.Response
已完成渲染,修改內部數據不起作用 - 若重新構造一個
rest_framework.response.Response
則會報未渲染錯誤,而渲染過程比較複雜 - 若選擇用
django.http.response.JSONResponse
重新構造響應則放棄了DRF的自動渲染特性
我對這些缺陷不能忍,於是想到了第二種途徑,也就是自定義renderer,它有以下好處:
- 即可全局生效(
DEFAULT_RENDERER_CLASSES
),又可針對單個APIView
生效,非常靈活 - 保留了DRF的智慧渲染特性,即瀏覽器請求渲染HTML頁面,後端請求渲染JSON響應
DRF的默認renderer有兩個:rest_framework.renderers.JSONRenderer
和rest_framework.renderers.BrowsableAPIRenderer
。這裡可以按需重載,如果瀏覽器和後端響應都需要,則都重載,如果只需要JSON響應,則重載第一個就可以了,這裡兩個類的重載點不一樣:
Python
class JSONRenderer(renderers.JSONRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): request = renderer_context['request'] # 在此處修改data return super().render(data, accepted_media_type=accepted_media_type, renderer_context=renderer_context) class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer): def get_content(self, renderer, data, accepted_media_type, renderer_context): request = renderer_context['request'] # 在此處修改data return super().get_content(renderer, data, accepted_media_type, renderer_context)
三、異常處理
我們經常會需要拋出異常,有些是主動拋出、有些是未捕獲的異常,在這些情況下,我們都希望日誌記錄異常的堆棧資訊,然後返回一個規範的響應(格式與上一節中一致),這樣我們就需要更改異常處理。在Django+DRF中異常處理有兩個重載點:
- 中間件中的
process_exception
函數 - DRF的
EXCEPTION_HANDLER
配置
而其中EXCEPTION_HANDLER
的作用時間早於中間件,這就導致了有些DRF內置的異常,在到達中間件之前已經渲染為正常的響應了,這明顯不是我們期望的效果,所以我們選擇第二個重載點。
Python
def exception_handler(exc, context): # copy自DRF默認exception_handler if isinstance(exc, Http404): exc = exceptions.NotFound() elif isinstance(exc, PermissionDenied): exc = exceptions.PermissionDenied() if isinstance(exc, exceptions.APIException): # DRF內置異常 headers = {} if getattr(exc, 'auth_header', None): headers['WWW-Authenticate'] = exc.auth_header if getattr(exc, 'wait', None): headers['Retry-After'] = '%d' % exc.wait if isinstance(exc.detail, (list, dict)): body = exc.detail message = str(exc) else: body = {} message = str(exc.detail) # copy結束 # 組裝響應體 return Response({...}) else: # 其他未捕獲異常 logger.error(traceback.format_exc()) if not isinstance(exc, ApiError): exc = ApiError(str(exc)) # 組裝響應體 return exc.as_response()
美中不足的是有一大段的程式碼是從DRF默認的異常處理函數copy過來的,這是DRF為數不多的不合理設計,留了一個配置項供你改變默認行為,但卻沒有留出一個好的重載點。
總結
DRY原則能使你的程式碼結構好、易維護、易擴展。在日常的開發中,要時刻反思自己的程式碼是否過於重複,可以精簡。在Python中,可以說只要你想,一定能把多處一樣的程式碼給抽取出來。只是有時候為了抽出這些程式碼,又產生了很多額外的程式碼,這是需要取捨的。相信本文中提到的三個大方向,能對你有所啟發。