本文中会介绍Django REST framework(后续统称DRF)中一些常用功能的使用方法以及框架中的基础概念。希望这些内容能够帮忙大家更好的运用该框架去实现API服务。 本文适合那些已经对Django框架、DRF以及RESTful API设计风格有了解的相关人群。关于Django框架,推荐直接去阅读官方文档,目前官方已经推出中文版的文档;对于DRF,笔者依旧还是推荐优先看官方文档(虽然没有中文版);对于RESTful API设计风格,可以查看笔者编写的<RESTful API设计经验总结>这篇文章。 笔者也会将自己的理解在文中进行阐述,这也算是在和大家交流心得的一个过程。若文中有错误的理解和概念,请大家及时纠正;吸纳大家的建议,对于我来说也是很重要的学习过程之一。


(目录)


1. 序列化器

正常情况下,客户端请求中的数据需要经过转换为model对象,并调用model对象的相关方法实现数据落地。反之,从数据库中获取到的数据(model)需要经过将model对象转换为python的基本数据结构后,交给view层逻辑进行包装并返回给客户端。 上述情况基本上会发生在每一个业务请求逻辑中,而这些操作逻辑基本上都相似。针对这种情况,应该思考是否可以把上述逻辑抽象出来作为通用逻辑;这样即可以减少重复编码,同时还可以规范数据转换的操作细节。在DRF中,就是使用序列化器来实现这种理念的。DRF中的序列化器同时肩负着序列化与反序列化的责任。

注意:本章节所谈的序列化器只负责model与python的基本数据结构之间的转化,而非客户端请求中的数据与python基本数据结构之间的转化。

1.1 实现技巧与思路

本章节中会介绍一些笔者在实现DRF的序列化器时所使用到的技巧和思路。对于实现序列化器的基本流程,笔者推荐去阅读官方文档进行学习。

1.1.1 只针对需要反序列化的字段进行操作

在针对某一个model编写序列化器之前,可以先对其中的字段进行分析。因为有些字段可能不需要返回给客户端,只是用于API服务内部逻辑处理使用或后台逻辑使用。要注意哪些仅仅是内部使用的字段,这些字段是没有必要反序列化的(对外展示)。 同时,也会有一些字段是只有在反序列化的时候是需要的。这是可以给该字段添加上如下属性:

records = serializers.ListField(write_only=True)

1.1.2 控制序列化的字段数量

序列化与反序列化时可能会使用到不同的字段,因此可以使用read_only与write_only属性来进行控制。 例如:

records = serializers.ListField(write_only=True)
id = serializers.IntegerField(read_only=True)

1.1.3 为可选字段设定default值

如果想做到每次只更新部分字段的功能,那么可以为相应的字段加上default值(一般为None)。 这样在后续的逻辑中就可以根据该字段是否为默认值来判断前端是否传入了该字段。

1.1.4 隐藏数据库字段名

如果不想把数据库字段名称对外暴露的话,可以使用source参数为指定model字段起一个用于外部使用的别名。

name = serializers.CharField(source="outside_name", read_only=True)

1.1.5 对关联字段的序列化定义

如果一个model中的某一个字段为关联关系字段(o2m,m2m等),若此时需要为该字段进行序列化时,DRF内置的常用序列化字段是无法实现的。需要开发者对该字段实现自定义序列化逻辑,即使用SerializerMethodFiled()。 在使用SerializerMethodFiled的同时,还需要编写一个配套的get_<序列化字段名>方法。而真正对于该字段的序列化逻辑,就是实现在这个get方法中的。即该方法返回什么,这个字段的序列化后就是什么样子;一般用于控制1对多或多对多的字段的序列化操作。

class ChargeSerializer(serializers.Serializer):

    id = serializers.IntegerField(read_only=True)
    charge_type = serializers.SerializerMethodField(read_only=True)
    c_type = serializers.IntegerField(write_only=True, required=False)
    threshold = serializers.DecimalField(max_digits=30, decimal_places=15)
    third_service_id = serializers.IntegerField(write_only=True, required=False)  # 用于反序列化的字段
    third_party_service = serializers.SerializerMethodField(read_only=True) # 用于序列化的字段

    def get_third_party_service(self, obj):
	# 自定义序列化逻辑
        tps_serializer = ThirdPartyServiceReadOnlySerializer(obj.third_service)
        return tps_serializer.data

这里笔者提供一个实现思路供大家参考: 关键是在于为关联关系字段在序列化器中建立两个对应的字段

  1. 专用反序列化字段 处理反序列化时的字段可以采用录入类似于主键或者uuid的方式,即该字段的类型为IntegerField、CharField或者是ListField类型。之后在create和update方法中再使用这些id去获取真正的model对象用于save等操作。该字段使用write_only=True参数来实现。
  2. 专用序列化字段 处理序列化时的字段可以使用SerializerMethodField类型的字段定义。通过在对应的get_<序列化字段名>的方法中编写将对应的model对象转换成dict类型来表示即可。该字段使用read_only=True参数来实现。

1.1.6 数据校验

反序列化时需要进行数据校验,方法is_vaild()只有反序列化的时候才需要调用。 可以针对每一个字段编写对应的校验方法,也可以对所有字段编写统一的校验方法(该方法中还可以加入自定义逻辑来满足特殊需求)。

1.1.7 对于何时使用serializers.ModelSerializer

可以在如下情况中考虑使用: 1. 当一个序列化器只用做反序列化时,则可以考虑通过继承serializers.ModelSerializer来快速实现该序列化器。 2. 当一个model没有多对多关系时,可以考虑使用模型序列化器自动生成。

注意:ModelSerializer序列化外键关系的字段时候是使用primary keys作为关系描述内容,即序列化后外键关系使用主键描述的

1.1.8 Model逻辑封装

可以将model相关的逻辑在序列化器中(validate,create或者update)实现。因为序列化器本身就和model关系密切,因此将model相关的封装写到其内部也合理。同时,这样也能大大精简view的编写。

1.1.9 关于Update方法的实现

为了尽可能的减少对数据库的操作,可以采取如下措施: 1. 在update方法中逐个检查参数是否合法,若不合法则直接返回参数不合法异常。 2. 在使用序列化器时可加入partial=True,允许部分字段更新(但这可能也需要前端的相关逻辑支持)。

核心理念即为尽量在数据入库之前进行参数合规检查,尽量不要让数据库去检查字段值是否合法;这样可以尽可能的降低数据库压力。

1.1.10 对于反序列化时使用到的字段的定义

建议这一类的字段都加上required=False, default=None这两个参数,以便于前端更灵活的传参和后续逻辑更简单的编写。

1.1.11 自定义数据不合法的返回

重写validate()方法即可。 如果没有将所有的字段都自定义校验,则最好是调用父类的validate()方法:

return super().validate(attrs)

1.1.12 校验必填字段的方式

  1. 利用serializers field的required参数 使用这种方式时,如果create和update操作涉及到的必填字段不同,那么就要为两种逻辑建立两个write_only=true的字段。即每种操作使用不同的字段去处理逻辑。
  2. 利用validate()方法与serializers field的default参数 。这种方法即是在validate()方法中检查必填参数的值是否为default的值;这种方法比较适用于create和update操作涉及到的必填字段相同。
  3. (推荐)将检验的逻辑放入create()方法与update方法中。这种方法即是将校验必填字段的逻辑移入到create()方法与update方法中,在方法中检查validated_data中是否包含有相应必填参数的key。这么做的前提是必填字段的不能带有default参数以及required=True。

1.1.13 关于数据校验

  1. 一般场景 数据校验一般是在创建model实例或者是更新model实例时使用。用于检查数据是否合法,符合各个字段的要求。
  2. 检查数据内容是否符合特殊要求 检查数据的内容是否符合特殊需求,如果符合则触发特殊逻辑。而并非时检查数据是否合法。
  3. 关于校验方法validate 该方法的形参attrs中的内容,依据的是当前序列化类描述的字段。即如果有某个字段,序列化类没有声明,那么attrs里也不会有这个参数。
  4. 关于序列化类中声明的字段名 如果继承的是rest_framework.serializers.Serializer:序列化类中声明的字段名必须要与反序列化时传入的各个参数名相互对应; 如果继承的是rest_framework.serializers.ModelSerializer:序列化类中声明的字段名必须要与相应的model类的各个字段名相互对应(如果没有使用source参数)

1.2 序列化实现流程

序列化主要是将把model对象转换成python的基本数据结构。

流程如下:

  1. 获取model类对象实例 若为新建对象:应先调用model的create()创建对象,同时获得新model对象实例。 若为查询或者修改操作:可通过客户端请求中的查询数据来获取指定的model对象实例。
  2. 在view类中调用序列化器 需要先创建对应的序列化器实例对象。如果使用了ViewSet功能,则可以直接view类中使用序列化器实例对象。 如果是批量序列化,则需要添加many=True参数;同时传入多个对象(query_set)即可。 序列化后的数据保存在序列化器对象实例的data属性内,该属性为一个dict类型变量。
  3. 构造Http Response 在获得了model类对象实例的序列化数据后,还需要将其放入Http Response后才能返回给客户端。 若无需对序列化器返回的序列化数据进行调整,则可以直接使用DRF自带的Response对象进行封装。 若需要对序列化器返回的序列化数据进行调整(例如规范返回数据的内容格式等等),则需要自行实现Response对象实现自定义返回体的内容。

注意:当序列化时指定了many=True,序列化器实例的data属性返回的是一个list,而非dict。

1.3 反序列化实现流程

反序列化主要是将python的基本数据结构转成model对象。

流程如下:

  1. 在view类中调用序列化器 需要先创建对应的序列化器对象实例。 如果使用了ViewSet功能,则可以直接view类中使用序列化器实例对象。
  2. 校验客户端数据 调用校验方法is_valid()进行参数校验。
  3. 操作model对象实例 调用序列化器对象实例的save方法操作model对象并将其数据进行落地。 在调用序列化器时,不同的操作场景会有不同的使用方式:
    1. 若是新建对象,使用客户端提供的的数据调用save()方法即可。
    2. 若是修改对象,则需要先获取需要修改的model对象实例,然后再使用该对象和请求数据调用save方法。

2. View

2.1 views.APIView

实际上,django cbv就已经能够自动根据request中的http method类型映射到相对应view中的处理方法了。

Tips: 这里需要注意的是,此时需要view类中的处理方法要以http method类型来进行命名。 例如PUT类型的http request必须要对应实现一个叫做put的方法。

DRF APIView的as_view()方法在Django CBV实现的功能基础上,还额外添加了针对models.query.QuerySet和CSRF相关的处理逻辑。

from django.views.generic import View

class APIView(View): # APIView是继承Django CBV的View而实现的
   …………
    @classmethod
    def as_view(cls, **initkwargs):
        """
        Store the original class on the view function.

        This allows us to discover information about the view when we do URL
        reverse lookups.  Used for breadcrumb generation.
        """
        if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
            def force_evaluation():
                raise RuntimeError(
                    'Do not evaluate the `.queryset` attribute directly, '
                    'as the result will be cached and reused between requests. '
                    'Use `.all()` or call `.get_queryset()` instead.'
                )
            cls.queryset._fetch_all = force_evaluation

        view = super().as_view(**initkwargs) # 调用django cbv的as_view()方法
        view.cls = cls
        view.initkwargs = initkwargs

        # Note: session based authentication is explicitly CSRF validated,
        # all other authentication is CSRF exempt.
        return csrf_exempt(view)
	…………

在功能扩展方面,APIView内增添了诸如用户认证、鉴权等新功能。 实际上这些功能也就是DRF主推的新功能。

    …………

    def perform_authentication(self, request):
        """
		设定用户认证方式
        Perform authentication on the incoming request.

        Note that if you override this and simply 'pass', then authentication
        will instead be performed lazily, the first time either
        `request.user` or `request.auth` is accessed.
        """
        request.user

    def check_permissions(self, request):
        """
		设定鉴权方式
        Check if the request should be permitted.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )

    def check_throttles(self, request):
        """
        Check if request should be throttled.
        Raises an appropriate exception if the request is throttled.
        """
		…………
        if throttle_durations:
            # Filter out `None` values which may happen in case of config / rate
            # changes, see #1438
            …………
            duration = max(durations, default=None)
            self.throttled(request, duration)

    …………

为了能够对接这些新功能。DRF还对接收到的http request以及Django CBV中dispatch()方法等等细节进行了改造和优化。

    …………

    def initialize_request(self, request, *args, **kwargs):
        """
		重新构造http request
        Returns the initial request object.
        """
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )

    def initial(self, request, *args, **kwargs):
        """
        Runs anything that needs to occur prior to calling the method handler.
        """
		………

        # Ensure that the incoming request is permitted
		# 接入新功能
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)

     def dispatch(self, request, *args, **kwargs):
        """
		优化Django CBV的dispatch()方法
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = wargs
        request = self.initialize_request(request, *args, **kwargs) # 重构http request
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

    …………

2.2 generics.GenericAPIView

GenericAPIView实际上是将DRF中的序列化器概念与APIView建立了依赖关系。 比起APIView,通过使用GenericAPIView就可以很方便的调用DRF的序列化器来处理request中的数据或生成response中的数据。 同时GenericAPIView还实现了例如分页查找等与django的QuerySet概念相关的功能。

from rest_framework import mixins, views

class GenericAPIView(views.APIView): # GenericAPIView是通过继承APIView来实现的

 queryset = None
 serializer_class = None
 pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

 ………

    def get_serializer(self, *args, **kwargs):
        """
		关联DRF的序列化器概念
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        kwargs.setdefault('context', self.get_serializer_context())
        return serializer_class(*args, **kwargs)

    def get_serializer_class(self):
        """
        Return the class to use for the serializer.
        Defaults to using `self.serializer_class`.

        You may want to override this if you need to provide different
        serializations depending on the incoming request.

        (Eg. admins get full serialization, others get basic serialization)
        """
        assert self.serializer_class is not None, (
            "'%s' should either include a `serializer_class` attribute, "
            "or override the `get_serializer_class()` method."
            % self.__class__.__name__
        )

        return self.serializer_class

    def paginate_queryset(self, queryset):
        """
		实现分页查找功能
        Return a single page of results, or `None` if pagination is disabled.
        """
        if self.paginator is None:
            return None
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

 ………

GenericAPIView另一个优势是其与DRF提供的相应Mixin配合后实现的特定http method APIView,这些http method APIView的特点是在其内部DRF已经提前实现好了对应的处理方法。 实际上,这些处理方法都是基于相应Mixin类中的相关方法实现的;而再进一步来看,Mixin类中的相关方法又是基于DRF的序列化器来实现的。因此这也是为什么这些APIView是需要继承GenericAPIView而实现的原因。 这些http method APIView可以帮助开发者快速的实现相应的基于restful风格的API

from rest_framework import mixins, views

 ………

class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """
    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)


class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    """
    Concrete view for listing a queryset.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)


class UpdateAPIView(mixins.UpdateModelMixin,
                    GenericAPIView):
    """
    Concrete view for updating a model instance.
    """
    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)


class ListCreateAPIView(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        GenericAPIView):
    """
    Concrete view for listing a queryset or creating a model instance.
    """
    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

 ………

对于在GenericView与APIView之间的选型上,笔者给出以下几种思考方向:

  1. 是否需要使用DRF的序列化器 若实现了DRF的序列化器,则建议在实现View选择继承GenericView来进行实现。 若无需使用DRF的序列化器,则建议可直接继承APIView来实现View。

  2. 基于GenericView的DRF内置APIView是否能够满足需求 DRF通过组合GenericView和一些内置的mixin的方式实现了一些常用http method对应的内置APIView。如果这些通用内置APIView能够满足当前需求,则建议使用这些内置APIView来实现API。这样能够大大减少编码工作量,快速实现业务需求。

2.3 ViewSet

2.3.1 优势

ViewSet的优势的优势在于可以减少封装的CBV数量以及规整合并相关的请求处理逻辑。

如果不使用viewset而是使用APIView(或使用Django CBV)来实现view的话,那么必须要使用http method来命名相应方法(例如get,put等)。 这样的实现机制就会导致需要为每一种http method都单独实现一个指定的CBV,即该CBV只能处理一类http method的请求。 上述这种实现会使APIView的数量大幅增加,这样会不便于阅读理解和维护。

而ViewSet的概念就是用来解决上述问题的。顾名思义,ViewSet相当于是多个CBV的功能集合。 通过使用viewset来编写view可以将处理不同类型的http method放到同一个ViewSet中,同时这些处理方法不再强求使用http method来进行命名

2.3.2 实现原理

ViewSet是通过继承APIView和ViewSetMixin来实现的。其本身内部并没有实现任何逻辑,全部是基于两个父类提供的功能来运转的。 ViewSet之所以能够实现上述功能是因为其再次重写了as_view()方法

Tips: ViewSet的绑定功能实际上都是在ViewSetMixin类中实现的,相当于是给APIView拓展了绑定功能。

ViewSet允许使用者实现自定义一个http method与自定义处理方法的映射关系,之后ViewSet即可按照用户自定义的映射关系来按照http method类型调用相应的处理方法。

 ………

class ViewSetMixin:
    """
    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'})  
    """

    @classonlymethod
    def as_view(cls, actions=None, **initkwargs):
        """
        Because of the way class based views create a closure around the
        instantiated view, we need to totally reimplement `.as_view`,
        and slightly modify the view function that is created and returned.
        """
        # The name and description initkwargs may be explicitly overridden for
        # certain route configurations. eg, names of extra actions.
         = None
        cls.description = None

		………

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)

            if 'get' in actions and 'head' not in actions:
                actions['head'] = actions['get']

            # We also store the mapping of request methods to actions,
            # so that we can later set the action attribute.
            # eg. `self.action = 'list'` on an incoming GET request.
            self.action_map = actions

            # Bind methods to actions
            # This is the bit that's different to a standard view
            for method, action in actions.items():  # 按照用户自定义的映射关系来调用相应处理方法
                handler = getattr(self, action)
                setattr(self, method, handler)

            self.request = request
            self.args = args
            self.kwargs = kwargs

            # And continue as usual
            return self.dispatch(request, *args, **kwargs)

		………

        # We need to set these on the view function, so that breadcrumb
        # generation can pick out these bits of information from a
        # resolved URL.
        view.cls = cls
        view.initkwargs = initkwargs
        view.actions = actions
        return csrf_exempt(view)

 ………

ViewSet实现的这种理念,可以让开发者使用业务维度的思维来实现View。 例如将某一类资源CURD的restful请求都封装实现在一个ViewSet中,或是将在业务层面关系比较紧密的一些API放在一个ViewSet中进行实现等等。

2.3.3 viewsets.GenericViewSet

由于GenericViewSet是通过继承GenericAPIView和ViewSetMixin来实现的,GenericViewSet与ViewSet的区别就与GenericView与APIView区别相似。 即GenericViewSet是与DRF中的序列化器概念建立了依赖关系的。

 ………
from rest_framework import generics, mixins, views

class ViewSetMixin:
  …………

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

 ………

因此,如果需要使用DRF的序列化器,则选用GenericViewSet来会更为方便。因为GenericViewSet(实际上是GenericView)中已经实现了很多关于序列化器的功能。 同理,如果只想实现自定义http method方法名时,则自定义ViewSet直接继承rest_framework.viewsets.ViewSet即可。

对于ModelViewSet的使用: 由于该视图类继承的mixin中的各个操作方法,都是写好的固定逻辑;因此肯定会有不符合自定义逻辑的地方。 此时,可以尝试仿写rest_framework.mixins中的各个方法;其次再让自定义view继承自定义的mixin类与GenericViewSet类;这样就可以按照类似于ModelViewSet的方式去写view了。

2.3.4 关于Mixin

在Django以及DRF内部,有很多的CBV都使用了Mixin来实现的。包括笔者在使用这两者去实现项目需求时,也会仿照它们去实现一些Mixin类。通过实践,笔者也体会到了Mixin的一些特点与优势,在这里给大家分享一下。

Mixin是利用了Python的多重继承机制来实现的

Mixin的目的就是给一个类增加多个功能。这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixin的功能,而不是设计深层次的复杂继承关系。可以将常用的逻辑进行通用化的抽象,并将这些逻辑封装到Mixin中。当view需要使用到这些功能时,就可以直接继承相应的Mixin类并直接调用相应的方法即可。

  • 使用Mixin模式有几点是需要注意:
  1. 它必须表示某一种功能,而不是某个物品。
  2. 它必须责任单一。如果有多个功能,那就写多个Mixin类
  3. 不依赖于子类的实现
  4. 子类即便没有继承这个Mixin类,也照样可以工作。就是缺少了某个功能。

笔者对上述几点的理解是:Mixin里实现的不是一个子类,而是指封装了一些功能方法的工具类。继承Mixin实际上是为了给子类添加更多的功能,而不是传统的继承父类。即Mixin类是无需实例化的,Mixin类无法单独作为一个类进行对象实例化使用。

对于Mixin的使用细节,这里笔者谈及一点:在继承mixin类时,务必要把所有的mixin类写在继承队列的最前端。这主要是由于Python的多重继承是通过C3算法实现的。如果所有类中没有同名的方法,那可以不用按照上述的要求进行编写。


3. 自定义认证逻辑

3.1 实现思路

DRF支持开发者实现和对接自定义的用户认证方式,其实现方式如下:

  1. 自定义认证类需要继承rest_framework.authentication.BaseAuthentication
  2. 需要重写authenticate方法以及authenticate_header方法 authenticate方法中主要是写自定义认证的逻辑。 例如在方法中实现从request请求中获取认证信息并判断其合法性。

另外,在用户信息认证失败时,可无需执行实现相关错误返回/异常;建议可选择使用DRF提供的一些内置异常来处理这种情况。例如rest_framework.exceptions.AuthenticationFailed,rest_framework.exceptions.PermissionDenied等等。

3.2 使用方式

在实现了自定义的认证类后,还需要在View层中对其进行调用才能实现对用户认证的功能。 对接/开启该功能有两种方式:

  1. 全局启用 需要在setting中进行配置。 例如:
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ]
}

注意:这里需要填写自定义类全路径的字符串描述,而非自定义认证类对象。

  1. 局部启用 在继承了APIView的视图中使用authentication_classes类属性来配置(实际上为APIView的类属性)。 authentication_classes的类型为list,其内容为自定义认证类对象:
from rest_framework.authentication import SessionAuthentication, BasicAuthentication

class ExampleView(APIView):
	authentication_classes = [SessionAuthentication, BasicAuthentication]

如果想关闭某一View中的认证机制时,可以将该APIView的authentication_classes置为空列表,以此来表示关闭认证逻辑:

class ExampleView(APIView):
	authentication_classes = []	

在对接/开启自定义认证逻辑后,就可在view中使用自定义认证的相关数据。例如: 由于自定义认证类中的authenticate方法返回的tuple中的内容会相应的保存到drf request的user和auth中(即self.user, self.auth);因此可以直接在后续的代码中通过使用request.user或request.auth来使用自定义认证返回的数据。

3.3 实现JWT认证

在DRF中的JWT认证方式主要需要实现两个功能:解析Request中的JWT以及为client签发JWT。其中: 1. 解析Request中的JWT是负责验证用户提供的JWT信息是否合法。该功能主要是由自定义的JWT 认证类来负责实现的。 2. 为client签发JWT是service为合法用户/client下发JWT,以便client后续可通过JWT进行身份合法验证。这一部分是通过相关的自定义API来实现的。

Tips: 需要事先安装好djangorestframework_jwt包。

3.3.1 实现自定义JWT认证类

由于djangorestframework_jwt包中已经提供了一个其自带的DRF认证类(rest_framework_jwt.authentication.JSONWebTokenAuthentication)。因此如果该认证类能够满足需求,则可直接调用此类来实现解析JWT Request。

解析JWT Request的核心操作有三个:

  1. 从Request中获取到JWT信息 由于JWT信息往往都会放在Request的header中进行传输,因此获取操作一般都是围绕着获取和解析Request header来展开实现的。DRF中提供了一个获取Request header中的认证类信息的方法,即rest_framework.authentication.get_authorization_header。如果实际的Request header的数据结构满足该方法的要求,则可直接调用该方法来获取JWT信息。JSONWebTokenAuthentication实际上使用的就是该方法来获取的JWT信息。

  2. 解析JWT信息 当获取到JWT信息后,还需要按照相应的JWT规则将其解析才能获得JWT的payload部分。djangorestframework_jwt中的JWT认证类是通过jwt_decode_handler方法来解析验证JWT信息,而djangorestframework_jwt也支持开发者可以自定义的解析逻辑,即实现自定义的jwt_decode_handler方法;该方法往往会在认证类的authenticate中进行调用。在djangorestframework_jwt中jwt_decode_handler默认是使用rest_framework_jwt.utils.jwt_decode_handler方法,该默认方法能够实现对于json串格式的JWT的解析。

  3. 解析JWT payload以及验证用户合法性 用户信息数据会保存在JWT的payload中,例如将用户名或用户ID保存在payload中。因此在解析了JWT后,应从其payload中获取到用户数据并验证用户是否合法。若为合法用户,则应该创建对应的django user model对象实例,并返回给view层;若为非法用户,则应按照用户认证失败逻辑进行相关处理。这一步主要是对payload进行解析并提取其中的用户信息数据,因此这一步的相关逻辑是与payload的数据结构息息相关的;而payload的数据结构同时也会影响签发JWT的逻辑操作,毕竟签发JWT实际上就是在生成JWT。djangorestframework_jwt中提供了一些获取payload中的工具方法,例如:rest_framework_jwt.utils.jwt_get_username_from_payload_handler。

基于上述的核心操作,可有两种实现自定义JWT认证类的思路:

  1. 继承rest_framework_jwt.authentication.BaseJSONWebTokenAuthentication 如果只需自定义“从Request中获取到JWT信息”这一步骤,则可继承BaseJSONWebTokenAuthentication来实现自定义认证类。即实现类实例方法get_jwt_value(request)。因为BaseJSONWebTokenAuthentication中的authenticate方法内会调用该类的get_jwt_value来获取request中的JWT,但并没有给出该方法的实现细节。继承BaseJSONWebTokenAuthentication就代表着解析JW和解析JWT payload默认使用的是djangorestframework_jwt的默认方法。如果需要使用解析自定义的Http Request header或解析自定义的JWT payload数据结构,可在DRF settings中定义JWT_DECODE_HANDLER、JWT_PAYLOAD_HANDLER等配置项,然后在相应的代码段内引用并使用这些settings配置项即可。

  2. 继承rest_framework.authentication.BasicAuthentication 若想高度自定义JWT认证类,则与实现一般认证类的思路相同,即继承BasicAuthentication。此时,建议模仿BaseJSONWebTokenAuthentication以及JSONWebTokenAuthentication来实现自定义JWT认证类。具体来说,可以选择性的重写相关的实例方法。例如在其中加入新的异常捕获或是日志打印之类的功能性逻辑。

这里给出笔者曾经实践的自定义JWT认证类,供大家参考:

import jwt

from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.authentication import BaseAuthentication


logger = settings.LOGGER

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER

def get_authorization_header(request):
    """获取request中的jwt;支持从cookie中获取
    """
    auth = request.META.get('HTTP_AUTHORIZATION', b'')
    if not auth:
        auth = request._request.COOKIES.get('jwt', b'')
    if isinstance(auth, str):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth

class JWTAuthentication(BaseAuthentication):
    """JWT认证类
    """

    def authenticate(self, request):
        """JWT认证逻辑
        """
        jwt_value = get_authorization_header(request)

        if not jwt_value:
            (log_msg_to_json_str(
                msg='获取JWT失败:request header的Authorization字段未包含token;',
                data=request.data,
                client_ip=request.META.get('REMOTE_ADDR', '')
            ))
            raise AuthenticationFailed('登陆验证失败.请提供合法的登陆验证信息')

        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignatureError:
            (log_msg_to_json_str(
                msg='JWT验证失败:JWT已过期;JWT:{0}'.format(jwt_value),
                data=request.data,
                client_ip=request.META.get('REMOTE_ADDR', '')
            ))
            raise AuthenticationFailed('登陆信息已过期.请尝试重新登陆')
        except jwt.DecodeError:
            logger.warning(log_msg_to_json_str(
                msg='JWT验证失败:解析payload失败;JWT:{0}'.format(jwt_value),
                data=request.data,
                client_ip=request.META.get('REMOTE_ADDR', '')
            ))
            raise AuthenticationFailed('登陆验证失败.请提供合法的登陆验证信息')
        except jwt.InvalidTokenError:
            logger.warning(log_msg_to_json_str(
                msg='JWT验证失败:接收到非法token;JWT:{0}'.format(jwt_value),
                data=request.data,
                client_ip=request.META.get('REMOTE_ADDR', '')
            ))
            raise AuthenticationFailed('非法登陆.请提供合法的登陆验证信息')

        user = self.authenticate_credentials(payload, request)

        return (user, jwt_value)

    def authenticate_credentials(self, payload, request):
        """验证jwt payload中用户信息的合法性
        """
        user_model = get_user_model()
        username = jwt_get_username_from_payload(payload)

        if not username:
            logger.warning(log_msg_to_json_str(
                msg='JWT验证失败:无法获取payload中的username',
                data=request.data,
                client_ip=request.META.get('REMOTE_ADDR', '')
            ))
            raise AuthenticationFailed('非法登陆.请提供合法的登陆验证信息')

        try:
            user = user_model.objects.filter(name=username).first()
        except user_model.DoesNotExist:
            (log_msg_to_json_str(
                msg='JWT验证失败:用户名:{0}不存在'.format(username),
                data=request.data,
                client_ip=request.META.get('REMOTE_ADDR', '')
            ))
            raise AuthenticationFailed('登陆信息认证失败.用户名不存在')

        if not user.is_active:
            (log_msg_to_json_str(
                msg='JWT验证失败:用户:{0}未激活'.format(username),
                data=request.data,
                client_ip=request.META.get('REMOTE_ADDR', '')
            ))
            raise AuthenticationFailed('登陆信息认证失败.该用户名未激活,请联系客户激活该用户后再尝试登陆')

        return user

3.3.2 实现签发JWT API

client在首次请求时,需要先使用个人的用户名以及密码来请求获取JWT。server/service在验证用户的身份后会自动生成相应的JWT返回给client,用户在后续的请求中就可以使用JWT来作为身份认证信息了。基于上述逻辑,一般需要为server实现一个专用于签发JWT的API

该API的实现思路为:

  1. 实现JWT签发API的序列化器 由于client是通过请求相应API来获取JWT,因此JWT生成逻辑可以在相应的序列化器中来实现。 签发JWT的“签”即表示创建,而创建一个新JWT的核心操作有两个:
    1. 构建JWT的payload payload的数据结构是开发者可以自定义的,开发者可根据实际的业务需求来设计相应的JWT payload结构。djangorestframework_jwt提供了一个构造payload的工具方法,即rest_framework_jwt.utils.jwt_payload_handler;该方法会生成一个dict类型的payload数据。如果需要自定义payload生成逻辑,可以通过配置DRF settings中的JWT_PAYLOAD_HANDLER配置项来修改DRF的默认payload生成方法;并在相应的代码段内引用并使用这些settings配置项即可。
    2. 构建JWT 在构建出payload后,还要将payload,Header以及Segments组合起来形成JWT本体。djangorestframework_jwt提供了一个构造JWT本体的工具方法,即rest_framework_jwt.utils.jwt_encode_handler。如果在生成JWT需要使用密钥加密,则可以通过配置DRF settings中的JWT_PRIVATE_KEY和JWT_SECRET_KEY来添加密钥和加密串。 这里给出笔者曾经实践的自定义JWT认证类,供大家参考:
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings

from tsmp_admin.models import TsmpUser

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

class JWTLoginSerializer(serializers.Serializer):
    """
    JWT登陆序列化器
    """

    username = serializers.CharField(max_length=50)
    password = serializers.CharField(max_length=128)

    def validate(self, attrs):
        """
        用户校验与签发JWT

        @param attrs: request中的参数
        @type  attrs: dict
        """
        user = TsmpUser.objects.filter(name=attrs['username'], is_active=True).first()

        # 校验用户
        if not user:
            error_info = {'msg': '登陆校验用户失败:用户名不存在'}
            raise ValidationError(error_info)
        if not user.check_password(attrs['password']):
            error_info = {'msg': '登陆校验用户失败:密码错误'}
            raise ValidationError(error_info)

        # 签发JWT
        payload = jwt_payload_handler(user)
        jwt = jwt_encode_handler(payload)
        self.context['jwt'] = jwt
        self.context['user'] = user

        return attrs
  1. 实现JWT签发APIView 需要一个API以提供给client登录访问并获取JWT,该API由相应的APIView和上述的API序列化器来共同实现。因为生成JWT的逻辑已经由序列化器实现,所以APIView只需实现将JWT存放在Response中并返回给client即可。在实现APIView时有需要注意,必须将类属性authentication_classes至为空List。因为当前API是用于获取JWT,因此不能对其使用JWT认证机制,否则就会出现“自相矛盾”的情况。由于之前已经将JWT签发逻辑封装在对应的序列化器的validate方法中,那么在View中实现签发逻辑时就可以直接调用序列化器的参数校验逻辑。即view中不会出现其他不相关的逻辑,只需要调用序列化器和相关日志记录等。最后,在成功生成了JWT后,可通过使用自定义Http response或DRF的APIResponse来将JWT下发给用户。若想自定义JWT接口的Response,则可以重写rest_framework_jwt.utils.jwt_response_payload_handler方法。重写方式见源码方法注释内容(前提:如果使用的是自动签发功能)。 这里给出笔者曾经实践的view,供大家参考:
class LoginAPIView(ViewSet):
    """
    登陆API视图
    """
    authentication_classes = []


    def login(self, request):
        jwt_serializer = JWTLoginSerializer(data=request.data)
        try:
            jwt_serializer.is_valid(raise_exception=True) # 校验用户以及签发JWT
        except ValidationError as error:
            logger.warning(log_msg_to_json_str(
                msg='用户登陆失败;错误原因:数据校验不通过;exc_type:{0};details:{1}'.format(
                    type(error), error
                ),
                data=request.data
            ))      
            raise LoginError(detail='用户登陆失败;提供的用户数据不合法;Details:{0}'.format(error))
        jwt = jwt_serializer.context.get('jwt')
        user = jwt_serializer.context.get('user')

        return APIResponse(msg='登陆成功', http_code=200, username=, token=jwt)

4.自定义异常

DRF允许开发者使用自定义常来处理错误和失败情况。

DRF通过使用exception_handler来处理异常并返回Response。 若需要自行实现对异常的捕获、分析以及构造Response,则可以选择实现自定义的exception_handler。实现方式可以仿照DRF的exception_handler来进行,即rest_framework.views.exception_handler。建议以扩展DRF rest_framework.views.exception_handler的思路来实现自定义exception_handler;可在自定义exception_handler引入DRF的exception_handler,并在其引入之前加入自定义的异常处理。

在实现exception_handler时,可以从以下几个方面进行设计。

4.1 异常处理

如果自定义异常类的数量变多,并且需要对每种异常都进行必要的异常处理时;此时建议可以将异常处理这部分逻辑封装为函数/类,并在exception_handler中进行调用。这样的设计方式可使得exception_handler与各类异常处理逻辑进行了解耦,有助于支持后续新增异常处理逻辑的实现。

笔者这里给出一个实践思路,供大家参考:

  1. 首先为特定的异常编写异常处理器
class AuthErrorHandler(BaseAPIExceptionHandler):

    def handle(self):
        response = Response(
            {
                'ErrorCode': 'LOGIN-AUTH-401',
                'ErrorMessage': str(self.exc)
            },
            status=status.HTTP_401_UNAUTHORIZED
        )

        return response
  1. 将第一步中实现的异常处理器注册到异常处理注册表中
# 需特殊处理的异常
# 以dict的形式注册到该list中
# dict中,key:exception的value为异常类,key:exception的value为相应的异常处理器
error_type = [
    {
        'exception': BaseAPIException,
        'handler': ZoneBankErrorHandler
    },
    {
        'exception': AuthenticationFailed,
        'handler': AuthErrorHandler
    },
    {
        'exception': ValidationError,
        'handler': SerializerErrorHandler
    },
    {
        'exception': ParseError,
        'handler': RequestParseErrorHandler
    },
    {
        'exception': MethodNotAllowed,
        'handler': MethodNotAllowedErrorHandler
    },
]
  1. 编写自定义exception_handler方法
def api_exception_handler(exc, context):
    response = None

    # 处理特殊异常
    for error in error_type:
        if isinstance(exc, error['exception']):
            custom_handler = error['handler'](exc, context)
            response = custom_handler.handle()

    # 处理未知错误
    if not response:
        default_handler = DefaultErrorHandler(exc, context)
        response = default_handler.handle()

    return response

4.2 构造Response

可在exception_handler中进行统一构造,即实现了API对客户端返回数据的格式统一。 也可选择在对于每种自定义异常的处理时顺便完成Response的构造工作,即exception_handler只负责将异常处理返回的Response直接发送给客户端。

4.3 异常捕获设计思路

为了能够更好的记录服务日志,同时也能够更好的配合自定义异常的捕获和处理,可在vew层面进行一些如下的异常捕获和处理设计:

  1. 可为DRF创建一类专用的异常,而view以及功能模块层等内部依旧按需使用相应的异常。

  2. 当在view层内捕获到相应的异常类后,可先记录相应的服务日志信息和进行相关处理;但最后必须将这些异常类转换为DRF专用异常,并向上抛出。

view层抛出的异常最终会被exception_handler所捕获,即此时exception_handler只需处理DRF专用异常。基于这种设计,可使得exception_handler只关注如何处理一种异常,从而简化了exception_handler对于异常的处理逻辑;同时也能够更容易的规范其中的异常处理和Response构造,并且最后还能够利用上DRF的异常处理机制。


5. 自定义Http Response

DRF提供的内置Response类(rest_framework.response.Response)可以帮助开发者更高效的构造出符合API风格的Http Response。尤其是使用了DRF的APIView类时,强烈建议在实现这些APIView的过程中,最后的返回值是DRF的内置Response类对象实例。这样的实现方式能够更好的利用DRF的特性。

DRF内置Response类实际上是通过继承Django的django.template.response.SimpleTemplateResponse而实现的;而其父类正是在Django中常用的例如TemplateResponse这些response类的父类。DRF内置Response类能够接收python基本数据类型的数据,即能够接收未渲染过的数据,也是因为上述的继承实现方式。DRF内置Response类相当于是扩展了Django的Response类,因此Django的Response类的用法也可同样在DRF内置Response类上使用。

而如果DRF内置Response类不能很好的满足需求时,可以采用以下思路来实现自定义的Response:

  1. 创建自定义Response类 自定义Response需要继承rest_framework.response.Response。

  2. 重写构造方法 虽然可以DRF内置Response类对象实例的data实例属性来传入任意数据,但如果需要在工程项目中通过实体类来规范Response的数据格式时,最好的实现方式是将各类数据作为Response类对象实例的实例属性来进行填写/赋值。如果有这方面的需求可以考虑重新init方法,为自定义Response类新增一些实例属性且为其进行赋值。

注意:Response内的数据最终还是需要汇总/整合到一个python基本类型的变量中,并将其作为data调用父类的构造方法。

  1. (可选)新增功能性实例/静态方法 在构建Response时最主要是构造出其内部的数据格式。如果构建数据格式的逻辑操作相对复杂,可以选择建立一些功能性方法来辅助构建数据格式。

这里笔者给出一个自己的实践供大家参考:

from rest_framework.response import Response


class APIResponse(Response):
    """自定义http response
    """

    def __init__(
		self,
		msg='None',
		code=0,
		data=None,
		http_code=100,
		headers=None,
		**kwargs
     ):
        """构造方法

        @param code: 平台内部状态码, 默认为0
        @type  code: int, 可选
        @param data: response携带的数据, 默认为None
        @type  data: dict, 可选
        @param http_code: http状态码, 默认为100
        @type  http_code: int, 可选
        @param headers: response的header部分, 默认为None
        @type  headers: dict, 可选
        """
        result = {'code': code, 'msg': msg, 'http_code': http_code}

        if data:
            result['data'] = data

        if kwargs:
            if 'data' in result.keys():
                result['data'].update(kwargs)
            else:
                result['data'] = kwargs

        # 调用父类构造方法
        super().__init__(data=result, status=http_code, headers=headers)

6. DRF Http Request

6.1 与Django Http Request的关系

DRF内置的Request对象实际上是依赖于Django Http Request对象的。

注意:这里是以依赖注入的形式与Django Http Request对象建立了关联关系,而并不是通过类继承来建立关系的。

class Request:
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.

    Kwargs:
        - request(HttpRequest). The original request instance.
          即Django Http Request对象
        - parsers(list/tuple). The parsers to use for parsing the
          request content.
        - authenticators(list/tuple). The authenticators used to try
          authenticating the request's user.
    """

    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        assert isinstance(request, HttpRequest), (
            'The `request` argument must be an instance of '
            '`django.http.HttpRequest`, not `{}.{}`.'
            .format(request.__class__.__module__, request.__class__.__name__)
        )

        self._request = request # 依赖注入
        self.parsers = parsers or ()
     …………………

DRF Request对象的实例属性._request保存的就是其所依赖的Django Http Request对象,因此相当于是DRF对Django的Http Request进行了功能扩展。

在DRF中,若想获取Django Http Request对象的实例属性,可以直接利用DRF Request对象来进行获取。因为DRF Request对象实现了一个__getattr__方法,该方法就是专用于来访问Django Http Request对象的。 即DRF Request对象利用反射机制来实现获取Django Http Request对象的实例属性。

def __getattr__(self, attr):
        """
        If an attribute does not exist on this instance, then we also attempt
        to proxy it to the underlying HttpRequest object.
        """
        try:
            return getattr(self._request, attr) # 反射
        except AttributeError:
            return self.__getattribute__(attr)

6.2 Request数据的获取方式

DRF Request对象改造/优化了Django Http Request对象获取请求数据的操作。

  1. POST请求 POST请求中的数据可直接通过访问DRF Request对象的实例属性data进行获取。
class Request:
    ………
    @property
    def data(self):
        if not _hasattr(self, '_full_data'):
            self._load_data_and_files() # 根据数据类型获取请求数据
        return self._full_data
    ………
  1. GET请求 获取GET请求可直接通过访问DRF的实例属性query_params实例属性。 实际上,query_params是取自self._request.GET的变量值,即其返回的实际上Django Http Request对象的GET实例属性。
class Request:
    ………
    @property
    def query_params(self):
        """
        More semantically correct name for request.GET.
        """
        return self._request.GET
    ………