前言:该文章并不是完整的RestFramework源码剖析,而是基于在使用过程中碰到的一些问题,在需要解决这些问题的情况下去阅读了部分源码。
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',自动登录就会失效。
涉及到一个app和一个中间件:
'django.contrib.messages',
'django.contrib.messages.middleware.MessageMiddleware',
主要用于向template推送消息,如果未使用到template可以不使用该app
RESTframework的request对象提供灵活的请求解析,允许以处理表单的方式处理JSON数据或其他媒体类型的请求。request.data是不可变对象,只能读取,无法给QueryDict的值再次赋值
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.
"""
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进行了封装,添加了以下功能:
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'})
"""
序列化器是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_
方法来指定自定义字段级验证Serializer
。
这些方法采用单个参数,即需要验证的字段值。
您的validate_
方法应返回已验证的值或引发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操作。
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."]}