Django RESTframeowork源碼剖析

前言:該文章並不是完整的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_KEYREST框架設置自定義密鑰的名稱。

反序列化項目列表時,錯誤將作爲表示每個反序列化項目的詞典列表返回。

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."]}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章