前言:該文章並不是完整的RestFramework源碼剖析,而是基於在使用過程中碰到的一些問題,在需要解決這些問題的情況下去閱讀了部分源碼。
0X00 從請求到響應,Django到底做了些什麼
1、HTTP請求分爲請求報文和響應報文,請求報文由客戶端發出,服務端接收。然後經過一系列的處理,服務端將響應報文返回給客戶端。當Django收到HTTP請求報文時,會提取請求報文中的信息,並將其封裝爲HttpRequest對象,具體函數如下:
(1)wsgi.py,創建一個wsgi app實例
def get_wsgi_application():
"""
The public interface to Django's WSGI support. Return a WSGI callable.
Avoids making django.core.handlers.WSGIHandler a public API, in case the
internal WSGI implementation changes or moves in the future.
"""
django.setup(set_prefix=False)
return WSGIHandler()
# WSGIHandler是一個類,重載了__call__方法,是一個可調用對象,調用時會實例化request,response對象
(2)WSGIRequest類,在WSGIHandler中被實例化
class WSGIRequest(HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
# If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
# trailing slash), operate as if '/' was requested.
path_info = get_path_info(environ) or '/'
self.environ = environ
self.path_info = path_info
# be careful to only replace the first slash in the path because of
# http://test/something and http://test//something being different as
# stated in http://www.ietf.org/rfc/rfc2396.txt
self.path = '%s/%s' % (script_name.rstrip('/'),
path_info.replace('/', '', 1))
self.META = environ
self.META['PATH_INFO'] = path_info
self.META['SCRIPT_NAME'] = script_name
self.method = environ['REQUEST_METHOD'].upper()
self.content_type, self.content_params = cgi.parse_header(environ.get('CONTENT_TYPE', ''))
if 'charset' in self.content_params:
try:
codecs.lookup(self.content_params['charset'])
except LookupError:
pass
else:
self.encoding = self.content_params['charset']
self._post_parse_error = False
try:
content_length = int(environ.get('CONTENT_LENGTH'))
except (ValueError, TypeError):
content_length = 0
self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
self._read_started = False
self.resolver_match = None
2.request.user對象
Django中間件爲request對象添加user屬性,表示當前登錄的用戶,如果當前用戶未登錄,那麼request.user返回的是一個AnonymousUser對象。以下是添加user屬性的中間件,默認會加載該中間件。
# from django.contrib.auth.middleware import AuthenticationMiddleware
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import load_backend
from django.contrib.auth.backends import RemoteUserBackend
from django.core.exceptions import ImproperlyConfigured
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
def get_user(request):
if not hasattr(request, '_cached_user'):
request._cached_user = auth.get_user(request)
return request._cached_user
class AuthenticationMiddleware(MiddlewareMixin):
"""
使用該中間件必須要加載session middleware
"""
def process_request(self, request):
assert hasattr(request, 'session'), (
"The Django authentication middleware requires session middleware "
"to be installed. Edit your MIDDLEWARE%s setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before "
"'django.contrib.auth.middleware.AuthenticationMiddleware'."
) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
request.user = SimpleLazyObject(lambda: get_user(request))
可以看到,默認會先訪問request._cached_user對象,如果不存在該對象使用auth模塊的get_user方法生成一個user實例。下面是auth模塊的get_user方法:
def get_user(request):
"""
Return the user model instance associated with the given request session.
If no user is retrieved, return an instance of `AnonymousUser`.
"""
from .models import AnonymousUser
user = None
try:
user_id = _get_user_session_key(request)
backend_path = request.session[BACKEND_SESSION_KEY]
except KeyError:
pass
else:
if backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
user = backend.get_user(user_id)
# Verify the session
if hasattr(user, 'get_session_auth_hash'):
session_hash = request.session.get(HASH_SESSION_KEY)
session_hash_verified = session_hash and constant_time_compare(
session_hash,
user.get_session_auth_hash()
)
if not session_hash_verified:
request.session.flush()
user = None
Login方法:
def login(request, user, backend=None):
"""
Persist a user id and a backend in the request. This way a user doesn't
have to reauthenticate on every request. Note that data set during
the anonymous session is retained when the user logs in.
"""
session_auth_hash = ''
if user is None:
user = request.user
if hasattr(user, 'get_session_auth_hash'):
session_auth_hash = user.get_session_auth_hash()
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash and
not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
else:
request.session.cycle_key()
try:
backend = backend or user.backend
except AttributeError:
backends = _get_backends(return_tuples=True)
if len(backends) == 1:
_, backend = backends[0]
else:
raise ValueError(
'You have multiple authentication backends configured and '
'therefore must provide the `backend` argument or set the '
'`backend` attribute on the user.'
)
else:
if not isinstance(backend, str):
raise TypeError('backend must be a dotted import path string (got %r).' % backend)
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
request.session[BACKEND_SESSION_KEY] = backend
request.session[HASH_SESSION_KEY] = session_auth_hash
if hasattr(request, 'user'):
request.user = user
rotate_token(request)
user_logged_in.send(sender=user.__class__, request=request, user=user)
那麼是如何把seesionid轉換成user的?
是因爲在setting.py中有個配置,如下圖:在INSTALLED_APPS中有一個'django.contrib.sessions'APP,這個APP是會對每次request和response請求做攔截,攔截到瀏覽器過來的request的時候,就會在裏面找到sessionid,找到sessionid之後,通過查詢數據庫,找到session_data,對數據進行解密,便可以直接把user取出來。
如果註釋掉'django.contrib.sessions',自動登錄就會失效。
0X01 Django消息框架
涉及到一個app和一箇中間件:
'django.contrib.messages',
'django.contrib.messages.middleware.MessageMiddleware',
主要用於向template推送消息,如果未使用到template可以不使用該app
0X02 RESTframeowrk的request對象
RESTframework的request對象提供靈活的請求解析,允許以處理表單的方式處理JSON數據或其他媒體類型的請求。request.data是不可變對象,只能讀取,無法給QueryDict的值再次賦值
0X03 APIView
Q:在AuthnticationMiddleWare會封裝request.user屬性,但是使用REST framework和jwt認證,禁用了該中間件仍然有user屬性,調查具體封裝在哪。邏輯如下:
執行視圖函數前->執行前置操作->(perform_authentication;check_permissions;check_throttles)->perform_authentication函數->rest framework request.py的@property user函數->如果request對象沒有user屬性->執行_authenticate方法->如果通過了認證,給request.user賦值。
def _authenticate(self):
"""
Attempt to authenticate the request using each authentication instance
in turn.
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise
if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple
return
self._not_authenticated()
rest framework的request類繼承了HTTPRequest並添加了許多功能(restframework.request):
class Request(object):
"""
Wrapper allowing to enhance a standard `HttpRequest` instance.
Kwargs:
- request(HttpRequest). The original request instance.
- parsers_classes(list/tuple). The parsers to use for parsing the
request content.
- authentication_classes(list/tuple). The authentications used to try
authenticating the request's user.
"""
0X04 DRF中的視圖和序列化器
PART1 視圖:
1.視圖繼承關係如下圖所示,DRF中的視圖都是繼承於Django的View,然後在此基礎之上封裝一系列功能。
2.引用一下DRF官方對APIView的描述:
REST框架提供了一個APIView類,它是Django View類的子類。APIView相較View有以下不同:
1)傳遞給處理程序方法的請求將是REST框架的Request
實例,而不是Django的HttpRequest
實例。
2)處理程序方法可以返回REST框架Response
,而不是Django HttpResponse
。
3)該視圖將管理內容協商並在響應上設置正確的渲染器。
4)任何APIException
例外都將被捕獲並調解爲適當的響應。
5)將對傳入的請求進行身份驗證,並在將請求分派給處理程序方法之前運行適當的權限和/或限制檢查。
使用APIView
該類與使用常規View
類幾乎相同,像往常一樣,傳入的請求被分派到適當的處理程序方法,如.get()
或.post()
。另外,可以在控制API策略的各個方面的類上設置許多屬性。
class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
# Allow dependency injection of other settings to make testing easier.
settings = api_settings
schema = DefaultSchema()
3.GenericAPIView
GenericAPIView進一步對APIView進行了封裝,添加了以下功能:
- 加入queryset屬性,可以直接設置這個屬性,不必再將實例化的courses,再次傳給seriliazer,系統會自動檢測到。除此之外,可以重載get_queryset(),這樣就不必設置'queryset=*',這樣就變得更加靈活,可以進行完全的自定義。
- 加入serializer_class屬性與實現get_serializer_class()方法。兩者的存在一個即可,通過這個,在返回時,不必去指定某個serilizer
- 設置過濾器模板:filter_backends
- 設置分頁模板:pagination_class
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
# You'll need to either set these attributes,
# or override `get_queryset()`/`get_serializer_class()`.
# If you are overriding a view method, it is important that you call
# `get_queryset()` instead of accessing the `queryset` property directly,
# as `queryset` will get evaluated only once, and those results are cached
# for all subsequent requests.
queryset = None
serializer_class = None
# If you want to use object lookups other than pk, set 'lookup_field'.
# For more complex lookup requirements override `get_object()`.
lookup_field = 'pk'
lookup_url_kwarg = None
# The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
# The style to use for queryset pagination.
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
4.GenericViewSet
GenericViewSet是對GenericAPIView的一個升級,用於整合視圖,減少代碼量。GenericViewSet重寫了as_view,使其能捕獲HTTP請求方法,並調用規定的函數。可以設置請求方法與函數的映射關係:
VIEWSET_CONF = {
'running_jar_list' : RunningJarViewSet.as_view({'get': 'list'}),
'jar_conf_list': JarConfViewSet.as_view({'get': 'list'}),
}
urlpatterns = [
#path(r'translog/', views.TranslogView.as_view()),
path(r'conf/', VIEWSET_CONF['jar_conf_list']),
path(r'running/', VIEWSET_CONF['running_jar_list']),
]
GenericViewSet:
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
"""
The GenericViewSet class does not provide any actions by default,
but does include the base set of generic view behavior, such as
the `get_object` and `get_queryset` methods.
"""
pass
上面提到的GenericViewSet的功能是由ViewSetMixin實現的:
class ViewSetMixin(object):
"""
This is the magic.
Overrides `.as_view()` so that it takes an `actions` keyword that performs
the binding of HTTP methods to actions on the Resource.
For example, to create a concrete view binding the 'GET' and 'POST' methods
to the 'list' and 'create' actions...
view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
"""
PART1 序列化器Serializer:
序列化器是rest framework中非常重要的一環,可以幫你進行數據序列化,數據校驗的工作
序列化器的作用:
(1)Python數據結構-> 合法性判定 -> 操作Model
(2)Model數據,QuerySet或QueryDict ->Python數據結構
一些小知識:
1.depth參數,指定遍歷查找關聯關係的深度,如果存在多層外鍵關係,通過指定深度可以返回外鍵對象的所有數據,而不是隻返回一個外鍵。
2.在反序列化數據時,您始終需要is_valid()
在嘗試訪問經過驗證的數據之前調用,或者保存對象實例。如果發生任何驗證錯誤,該.errors
屬性將包含表示結果錯誤消息的字典。例如:
serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']}
字典中的每個鍵都是字段名稱,值將是與該字段對應的任何錯誤消息的字符串列表。該non_field_errors
鍵也可能存在,並列出任何一般驗證錯誤。non_field_errors
可以使用NON_FIELD_ERRORS_KEY
REST框架設置自定義密鑰的名稱。
反序列化項目列表時,錯誤將作爲表示每個反序列化項目的詞典列表返回。
3.在驗證數據失敗時,拋出異常:
該.is_valid()
方法採用可選raise_exception
標誌,serializers.ValidationError
如果存在驗證錯誤,將導致其引發異常。
這些異常由REST框架提供的默認異常處理程序自動處理,並且默認情況下將返回HTTP 400 Bad Request
響應。
# Return a 400 response if the data was invalid.
serializer.is_valid(raise_exception=True)
4.爲某字段添加定製化驗證方法:
您可以通過向子類添加.validate_<field_name>
方法來指定自定義字段級驗證Serializer
。
這些方法採用單個參數,即需要驗證的字段值。
您的validate_<field_name>
方法應返回已驗證的值或引發serializers.ValidationError
。例如:
from rest_framework import serializers
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
content = serializers.CharField()
def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value
5.如果需要對多個字段進行驗證,或者多個字段之間有約束關係,可以重寫validate方法,該方法會在執行了默認的model和serializer約束後被執行
from rest_framework import serializers
class EventSerializer(serializers.Serializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()
def validate(self, data):
"""
Check that start is before finish.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data
6.部分更新
默認情況下,序列化程序必須傳遞所有必填字段的值,否則會引發驗證錯誤。您可以使用該partial
參數以允許部分更新。
# Update `comment` with partial data
serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True)
7.嵌套序列化
前面的示例適用於處理只有簡單數據類型的對象,但有時我們還需要能夠表示更復雜的對象,其中對象的某些屬性可能不是簡單的數據類型,如字符串,日期或整數。
所述Serializer
類本身的類型Field
,並且可以用於表示其中一個對象類型嵌套在另一個關係
class UserSerializer(serializers.Serializer):
email = serializers.EmailField()
username = serializers.CharField(max_length=100)
class CommentSerializer(serializers.Serializer):
user = UserSerializer()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
如果嵌套表示可以選擇接受該None
值,則應將該required=False
標誌傳遞給嵌套的序列化程序。
class CommentSerializer(serializers.Serializer):
user = UserSerializer(required=False) # May be an anonymous user.
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
同樣,如果嵌套表示應該是項列表,則應將該many=True
標誌傳遞給嵌套序列化
class CommentSerializer(serializers.Serializer):
user = UserSerializer(required=False)
edits = EditItemSerializer(many=True) # A nested list of 'edit' items.
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
8.嵌套序列化的寫操作
處理支持反序列化數據的嵌套表示時,嵌套對象的任何錯誤都將嵌套在嵌套對象的字段名稱下:
serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'user': {'email': ['Enter a valid e-mail address.']}, 'created': ['This field is required.']}
同樣,該.validated_data
屬性將包含嵌套數據結構。
嵌套序列化寫操作--create和update
class UserSerializer(serializers.ModelSerializer):
profile = ProfileSerializer()
class Meta:
model = User
fields = ['username', 'email', 'profile']
def create(self, validated_data):
profile_data = validated_data.pop('profile')
user = User.objects.create(**validated_data)
Profile.objects.create(user=user, **profile_data)
return user
1.繼承關係圖:
2.BaseSerializer
class BaseSerializer(Field):
"""
The BaseSerializer class provides a minimal class which may be used
for writing custom serializer implementations.
Note that we strongly restrict the ordering of operations/properties
that may be used on the serializer in order to enforce correct usage.
In particular, if a `data=` argument is passed then:
.is_valid() - Available.
.initial_data - Available.
.validated_data - Only available after calling `is_valid()`
.errors - Only available after calling `is_valid()`
.data - Only available after calling `is_valid()`
If a `data=` argument is not passed then:
.is_valid() - Not available.
.initial_data - Not available.
.validated_data - Not available.
.errors - Not available.
.data - Available.
"""
3.使用serializer來進行數據合法性校驗(沒有model層,僅僅校驗前臺輸入的數據合法性):
數據合法性校驗應該全部放到serializer層處理比較合理,這樣不需要在視圖層寫複雜的數據合法性判斷邏輯
class InterfaceSerializer(serializers.Serializer):
ip = serializers.RegexField('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')
mask = serializers.RegexField('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')
seri = InterfaceSerializer(data={'ip':'192.168.2.2', 'mask':'qwe'})
seri.is_valid() # False
4.ModelViewSet的create方法和update方法
create方法和update方法繼承於mixins.CreateModelMixin和mixins.UpdateModelMixin。兩個方法都會調用serializer.save(),該方法會根據是否存在model的實例instance來判斷執行create操作還是update操作。
0X05 DRF中的異常捕獲
1.APIException:DRF中所有異常的基類,基於Django的Exception類進行封裝,需要設置三個屬性:status_code,default_detail,default_code,默認是500:
class APIException(Exception):
"""
Base class for REST framework exceptions.
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = _('A server error occurred.')
default_code = 'error'
可以對其進行封裝,創建自己想要的異常信息:
from rest_framework.exceptions import APIException
class ServiceUnavailable(APIException):
status_code = 503
default_detail = 'Service temporarily unavailable, try again later.'
default_code = 'service_unavailable'
可以調用相應的方法來獲取異常的三個屬性:
.detail - 返回錯誤的文字說明。
.get_codes() - 返回錯誤的代碼標識符。
.get_full_details() - 返回文本描述和代碼標識符。
Example:
>>> print(exc.detail)
You do not have permission to perform this action.
>>> print(exc.get_codes())
permission_denied
>>> print(exc.get_full_details())
{'message':'You do not have permission to perform this action.','code':'permission_denied'}
2.自定義異常處理,例如需要在響應的信息中也顯示響應狀態碼:
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 62
{"status_code": 405, "detail": "Method 'DELETE' not allowed."}
那麼可以重寫exception_handler,該函數必須帶有兩個參數,第一個是要處理的異常,第二個是包含任何額外上下文的字典,例如當前正在處理的視圖。異常處理函數應該返回一個Response
對象,或者None
如果無法處理異常則返回。如果處理程序返回None
,則將重新引發異常,Django將返回標準的HTTP 500'服務器錯誤'響應。
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
response.data['status_code'] = response.status_code
return response
exception_handler源碼如下,可以得知,該異常處理器默認只處理APIException及其子類;Django默認的404錯誤;PermissionDenied異常。任何無法被捕獲的異常將直接造成500服務器錯誤。
def exception_handler(exc, context):
"""
Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also
Django's built-in `Http404` and `PermissionDenied` exceptions.
Any unhandled exceptions may return `None`, which will cause a 500 error
to be raised.
"""
if isinstance(exc, Http404):
exc = exceptions.NotFound()
elif isinstance(exc, PermissionDenied):
exc = exceptions.PermissionDenied()
if isinstance(exc, exceptions.APIException):
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)):
data = exc.detail
else:
data = {'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
return None
如果要自定義的異常處理器生效,需要添加下配置,如果未指定,默認就使用的是上面提到的exception_handler
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
需要注意的是,異常處理器只是在raise一個異常時被調用,並不會在視圖函數返回Response(status=status.HTTP_400_BAD_REQUEST)時被調用。
3.ValidationError,對APIException的封裝,收集在驗證數據合法性時產生的異常:
class ValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = _('Invalid input.')
default_code = 'invalid'
def __init__(self, detail=None, code=None):
if detail is None:
detail = self.default_detail
if code is None:
code = self.default_code
# For validation failures, we may collect many errors together,
# so the details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail]
# 此處保證detail是一個字典或者列表,返回多條記錄
self.detail = _get_error_details(detail, code)
驗證錯誤的處理方式略有不同,並將字段名稱作爲響應中的鍵。如果驗證錯誤不是特定於某個字段,那麼它將使用“non_field_errors”鍵,或者任何字符串value被設置爲NON_FIELD_ERRORS_KEY
任何示例驗證錯誤可能如下所示:
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 94
{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}