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

 

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