Django-16:rest-framework与jwt

  • 一、web开发模式
    • 1.1 前后端不分离
    • 1.2 前后端分离
  • 二、api接口
    • 2.1 postman
    • 2.2 Restful规范
  • 三、Django Rest Framework初识
  • 四、APIView
    • 4.1 CBV源码分析
    • 4.2 APIVIew源码分析
      • 4.2.1 总结
  • 五、序列化器Serializer
    • 5.1 GET获取数据(序列化)
      • 5.1.1 补充:如何批量序列化
    • 5.2 PUT修改数据(反序列化)
      • 5.2.1 补充:partial参数
    • 5.3 POST新增数据(反序列化)
    • 5.4 删除一个数据(不需要序列化)
    • 5.5 序列化类的字段类型
    • 5.6 序列化类的字段参数
      • 5.6.1 read_only和write_only
      • 5.6.2 继承ModelSerializer的只读只写
    • 5.7 钩子函数
    • 5.8 自己封装Response对象
    • 5.9 Serializer高级用法
  • 六、ModelSerializer
    • 6.1 fields属性
  • 七、Request与Response
    • 7.1 请求
    • 7.2 响应
  • 八、视图
    • 8.1 基于APIView写接口
    • 8.2 基于GenericAPIView写的接口
    • 8.3 GenericAPIView与5个视图扩展类
      • 8.3.1 分析
    • 8.4 GenericAPIView的9个视图子类
    • 8.5 ModelViewSet
      • 8.5.1 ViewSetMixin改写路由
  • 九、路由
    • 9.1 action的使用
  • 十、认证
    • 10.1 认证组件的使用
    • 10.2 ORM补充
      • 10.2.1 on_delete参数
      • 10.2.2 断关联
      • 10.2.3 抽象表
  • 十一、权限
    • 11.1 权限组件的使用
  • 十二、频率
    • 注:后续需要补充
  • 十三、过滤
  • 十四、排序
  • 十五、自定义异常处理
  • 十六、封装Response
  • 十七、练习:图书表的批量增删改查
  • 十八、分页器
    • 2.1 PageNumberPagination
      • 2.1.1 参数详解
    • 2.2 LimitOffsetPagination
      • 2.2.1 参数详解
    • 2.3 CursorPagination
      • 2.3.1 参数详解
    • 2.4 报错:'Manager' object is not subscriptable
    • 2.5 使用APIView或GenericAPIView
  • 十九、coreapi
  • 二十、jwt
    • 20.1 jwt基本认证使用
      • 20.1.1 获取token
      • 20.1.2 校验token
        • 20.1.2.1 内置校验
    • 20.2 自定义返回数据格式
      • 20.2.1 获取token
      • 20.2.2 校验token
      • 20.2.3 总结
    • 20.3 自动签发应用:多方式登陆
    • 20.4 token过期时间
    • 20.5 RBAC
    • 20.6 django缓存

一、web开发模式

1.1 前后端不分离

前后端混合开发(前后端不分离):返回的是html的内容,需要写模板

1.2 前后端分离

前后端分离:只专注于写后端接口,返回json,xml格式数据

  • 分离之后,后端只需要关注于返回的JSON数据即可

    xml格式如今用的特别少,因为没有JSON那么通透

    # xml格式
    <xml>
    <name>liuyu</name>
    </xml>
    
    // JSON格式
    {"name":"liuyu"}
    

什么是动态页面、静态页面:

  • 可以理解为,每次请求的时候,数据都可能不一样的,叫做动态页面。反之,数据都是写死的,叫做静态页面

    概念拓展:
    '''
    页面静态化: 先把页面渲染成一个静态页面,给所有人返回的时候就返回这个静态页面,当后端数据发生变化的时候再去数据库查询,重新生成一个静态页面,这样可以减轻服务器的压力。这种方式就叫做“页面静态化”。
    '''
    

二、api接口

什么是API接口:

  • 通过网络,规定了前后台信息交互规则的url链接,也就是前后台信息交互的媒介

2.1 postman

什么是postman:

  • Postman是一个接口测试工具,在做接口测试的时候,Postman相当于一个客户端,它可以模拟用户发起的各类HTTP请求,将请求数据发送至服务端,获取对应的响应结果,从而验证响应中的结果数据是否和预期值相匹配;并确保开发人员能够及时处理接口中的bug,进而保证产品上线之后的稳定性和安全性。

    它主要是用来模拟各种HTTP请求的(如:get/post/delete/put…等等),Postman与浏览器的区别在于有的浏览器不能输出Json格式,而Postman更直观接口返回的结果。

返回的JSON格式数据,如果太长不方便看出层级关系,那么可以在网页上使用一些JSON解析工具

  • 如:https://www.json.cn/

2.2 Restful规范

什么是RESTful:

  • RESTful是一种定义Web API接口的设计风格,尤其适用于前后端分离的应用模式中。

    这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。

Restful规范(10条)

一、数据的安全保障

  • url链接尽可能都采用https协议进行传输,采用https协议可以提高数据交互过程中的安全性。

二、接口特征表现

  • 一看就知道是个api接口,用api关键字标识接口url:

    如:

    • https://api.baidu.com
    • https://www.baidu.com/api

    看到api字眼,就代表该请求url链接是完成前后台数据交互的

三、多数据版本共存

  • 在url链接中标识数据版本

    如:

    • https://api.baidu.com/v1
    • https://api.baidu.com/v2

四、数据即是资源

  • 均使用名词(可复数)如:books、users, 而不是动词,如:get_booklist、get_userlist

    接口一般都是完成前后台数据的交互,交互的数据我们称之为资源

    • https://api.baidu.com/users
    • https://api.baidu.com/books
    • https://api.baidu.com/book

五、资源操作由请求方式决定

  • 操作资源一般都会涉及到增删改查,我们提供请求方式来标识增删改查动作

    如:

    • https://api.baidu.com/books - get请求:获取所有书
    • https://api.baidu.com/books/1 - get请求:获取主键为1的书
    • https://api.baidu.com/books - post请求:新增一本书书
    • https://api.baidu.com/books/1 - put请求:整体修改主键为1的书
    • https://api.baidu.com/books/1 - delete请求:删除主键为1的书

六、过滤

  • 通过在url上传参的形式传递搜索条件

    如:

    • https://api.example.com/v1/zoos?limit=10 :指定返回记录的数量

    • https://api.example.com/v1/zoos?offset=10 :指定返回记录的开始位置

    • https://api.example.com/v1/zoos?page=2&per_page=100 :指定第几页,以及每页的记录数

    • https://api.example.com/v1/zoos?sortby=name&order=asc :指定返回结果按照哪个属性排序,以及排序顺序

    • https://api.example.com/v1/zoos?animal_type_id=1 :指定筛选条件

七、返回响应状态码

  • 返回的JSON字符串中带响应状态码

    响应状态码:

    • 2xx :常规请求等

    • 3xx :重定向相关等

    • 4xx :客户端异常等

    • 5xx :服务器异常等

八、错误处理

  • 应返回错误信息,error当做key

    如:

    {error: "权限不足"
    }
    

    其实没必要全部都遵循,可以用例如“msg”等作为key

九、返回的结果(数据)处理

  • 返回的结果,针对不同操作,服务器向用户返回的结果应该符合以下规范“

    GET请求:

    • https://api.test.com/collection:返回资源对象的列表(数组)
      https://api.test.com/collection/resource:返回单个资源对象

    POST请求:

    • https://api.test.com/collection:返回新生成的资源对象
      https://api.test.com/collection/resource:返回完整的资源对象

十、需要url请求的资源需要访问资源的请求链接

  • Hypermedia API,RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么

    如:

    {"status": 0,"msg": "ok","results":[{"name":"肯德基(罗餐厅)","img": "https://image.baidu.com/kfc/001.png"}...]
    }

三、Django Rest Framework初识

Django Rest Framework(DRF)框架作用:

  • 核心思想: 缩减编写api接口的代码。

  • Django REST framework是一个建立在Django基础之上的Web 应用开发框架,可以快速的开发REST API接口应用。在REST framework中,提供了序列化器Serialzier的定义,可以帮助我们简化序列化与反序列化的过程,不仅如此,还提供丰富的类视图、扩展类、视图集来简化视图的编写工作。REST framework还提供了认证、权限、限流、过滤、分页、接口文档等功能支持。REST framework提供了一个API 的Web可视化界面来方便查看测试接口。

    基本的使用,以及序列化器Serialzier等其他内容,见其他章节。

安装:

win+R:

  • pip3 install djangorestframework

四、APIView

先分析推导,再介绍如何使用、能实现怎么作用以及实现原理。

在分析APIView源码之前,先从CBV的源码入手。

4.1 CBV源码分析

CBV:

# 视图层
from django.shortcuts import render,HttpResponse
from django.views import  View
class Index(View):def get(self,request):return HttpResponse('get')def post(self,request):print(request.POST)return HttpResponse('post')# 路由层
urlpatterns = [path('index',views.Index.as_view()),
]
  • 前文中已经介绍过了,所以之类就长话短说。

    突破口在于as_view(),因为直接就是加括号调用,所以查看源码发现,该函数是个闭包函数,返回内部的view函数,在该函数内部,又调用了我们上述案例代码中的Index类并实例化了一个对象名为self,随后view函数return了self.dispatch,查询dispatch源码可以看出,后续利用反射来获取当前访问方式所对应的视图函数,随后变量handler接收视图函数的内存地址,最后return的时候,handler加括号调用。

    def view(request, *args, **kwargs):#request是当次请求的requestself = cls(**initkwargs)  #实例化得到一个对象,index对象if hasattr(self, 'get') and not hasattr(self, 'head'):self.head = self.getself.request = requestself.args = argsself.kwargs = kwargsreturn self.dispatch(request, *args, **kwargs)def dispatch(self, request, *args, **kwargs):#request是当次请求的request   self是index对象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_allowedreturn handler(request, *args, **kwargs)  #执行get(request)
    

4.2 APIVIew源码分析

APIView模块也是继承了django原生View,使用方法也大体相同。

# 路由
urlpatterns = [path('index',views.Index.as_view()),
]# 视图
from django.shortcuts import render,HttpResponse
from rest_framework.views import APIViewclass Index(APIView):def get(self,request):return HttpResponse('get')def post(self,request):print(request.POST)print(request.data)return HttpResponse('post')

与CBV的切入点一样,也是先看as_view方法,但是由于现在Index类先继承的是APIView,然后才是原生View,所以这里按照面向对象属性查找就找到了APIView类中的as_view方法,如下:

#APIView的as_view方法(类的绑定方法)def as_view(cls, **initkwargs):view = super().as_view(**initkwargs)  # 调用父类(View)的as_view(**initkwargs)view.cls = clsview.initkwargs = initkwargs# 以后所有的请求,都没有csrf认证了,只要继承了APIView,就没有csrf的认证return csrf_exempt(view)

super().as_view(**initkwargs)调用父类的as_view方法,而APIView的父类就是原生View,这就又回到了CBV的底层代码。

饶了一圈之后,又执行到了View --> as_view --> view --> dispatch

def view(request, *args, **kwargs):self = cls(**initkwargs)  if hasattr(self, 'get') and not hasattr(self, 'head'):....return self.dispatch(request, *args, **kwargs)

但是由于首先继承的是APIView类,所以在调用dispatch的时候,会先从index类中找,没有就先去APIView,而此时APIView刚好就有dispath方法,所以后面执行的,并不是View–>dispath,而是APIView下的dispatch

# APIView的dispatch方法def dispatch(self, request, *args, **kwargs):self.args = argsself.kwargs = kwargs# 重新包装成一个request对象,以后再用的request对象,就是新的request对象了request = self.initialize_request(request, *args, **kwargs)self.request = request.......

APIViewdispatch方法中,先对request请求进行重新封装request方法,添加了一些属性和方法,如request.data,作用:可以拿到任何编码提交的数据,随后再经过三大模块认证校验,通过之后就执行与View–>dispatch一样的代码,利用反射获取到方法的地址。

附:

  • APIView中diapach方法的后半段,及initialize_request方法

    # APIView的dispatch方法def dispatch(self, request, *args, **kwargs):self.args = argsself.kwargs = kwargs# 重新包装成一个request对象,以后再用的request对象,就是新的request对象了request = self.initialize_request(request, *args, **kwargs)self.request = requestself.headers = self.default_response_headers  try:# 三大认证模块self.initial(request, *args, **kwargs)# Get the appropriate handler methodif 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)# 渲染模块'''区分出访问的客户端是浏览器还是POSTMAN,如果是浏览器,那么就将JSON数据展示在一个好看点的页面,如果是POSTMAN,那就直接返回JOSN数据,不做渲染'''self.response = self.finalize_response(request, response, *args, **kwargs)return self.response# APIView的initial方法def initial(self, request, *args, **kwargs):# 认证组件:校验用户 - 游客、合法用户、非法用户# 游客:代表校验通过,直接进入下一步校验(权限校验)# 合法用户:代表校验通过,将用户存储在request.user中,再进入下一步校验(权限校验)# 非法用户:代表校验失败,抛出异常,返回403权限异常结果self.perform_authentication(request)# 权限组件:校验用户权限 - 必须登录、所有用户、登录读写游客只读、自定义用户角色# 认证通过:可以进入下一步校验(频率认证)# 认证失败:抛出异常,返回403权限异常结果self.check_permissions(request)# 频率组件:限制视图接口被访问的频率次数 - 限制的条件(IP、id、唯一键)、频率周期时间(s、m、h)、频率的次数(3/s)# 没有达到限次:正常访问接口# 达到限次:限制时间内不能访问,限制时间达到后,可以重新访问self.check_throttles(request)
    

4.2.1 总结

只要继承了APIView,视图类中的request对象,都是新的,也就是下面那个request的对象,新的request对象用于更多的方法,如data等

​ request.data可以获取到任意编码提交过来的数据。

  • # 继承了APIView之后,视图类中的request对象,就成了下面这个。
    from rest_framework.request import Request
    
  <font color='MediumPurple1'>新request</font>与<font color='MediumPurple1'>老request</font>在用法上的区别,及源码分析:- 没有区别```pythondef __getattr__(self, attr):try:return getattr(self._request, attr) #通过反射,取原生的request对象,取出属性或方法except AttributeError:return self.__getattribute__(attr)

老request在新request的._request中,在调用如request.POST时,由于内部定义了__getattr__方法,当加点调用时,执行函数体内部的代码,随后通过反射来获取老request中的方法,在使用过程中无感,不影响使用。

附:

  • request.data其实是并不是数据属性,而是一个方法,只是用**@property**伪装了

  • POST请求的数据都被封装到了request.data中,那么GET请求呢?

    '''
    二选一
    '''
    print(request.GET)
    print(request.query_params)'''
    query_params源码
    '''
    @property
    def query_params(self):"""More semantically correct name for request.GET."""return self._request.GET
    

    继续使用get仍然可以,但是我们获取get请求值的时候,获取的是url后面跟的参数,所以query_params会更合理一点,于是便做了封装。

五、序列化器Serializer

在4.2的章节中,介绍了APIView源码,以及所做的效果,就是封装了request,并对其进行校验。

response在走的时候也针对访问客户端区别响应,如果是浏览器,那么就渲染出一个好看点的页面。

Serializer的作用:

  • 在4.2章节中,利用APIView可以完成对请求和响应的封装等操作,但是并没有将Queryset对象,序列化成JSON格式数据。

  • 所以Serializer的作用就是将后端查询出来的模型对象,也就是Queryset对象,转换成字典,随后经过response变成JSON格式的字符串,这个过程叫做序列化。

    有序列化就有反序列化,反序列化就是,客户端发送过来的数据,经过request之后,由JSON格式转换成python中的字典,随后Serializer序列化器将字典转成模型(Queryset)

    同时,Serializer还可以完成数据校验功能,对POST、PUT请求提交的数据做校验(支持自定义),类似于forms组件

5.1 GET获取数据(序列化)

步骤总览:

  • 新建py文件,或者再套一层文件夹,用来写一个序列化类,继承serializers.Serializer

  • 在类中写要序列化的字段,想序列化哪个字段,就在类中写哪个字段。

    假设有五个字段,如果只需要返回给前端三个字段的数据,那么序列化类中只需要写这三个字段就好。

  • 使用时,需要在视图类中导入,并实例化得到序列化类的对象,把需要序列化的对象传入

  • 序列化类的对象.data,可以得到一个字典,是由原来的Queryset对象转成的。

  • 把字典以JSON格式返回,所以需要使用JsonResponse,除了JsonResponse以外,还可以使用rest_framework提供的Response

    JsonResponse与Response的区别:

    • Response可以针对浏览器的请求,做出一些好看的页面,如下:

代码示例:

模型层代码略

一、新建序列化类

  • 本示例中,是创建了一个文件夹,文件夹内的py文件中书写了序列化类,具体的层级关系如下:

    # 序列化类   serializer.py文件
    from rest_framework import  serializersclass BookSerializer(serializers.Serializer):name = serializers.CharField()price = serializers.CharField()author = serializers.CharField()publish = serializers.CharField()
    

    由于需要将name字段、price字段、author字段、publish字段都通过Serializer序列为字典,所以这里就都写上了,

    CharField()里面可以设置参数,与models.py类似,可以设置最长多少、最短多少,可用于对提交数据做校验。

二、路由与视图层代码

  • 路由

    urlpatterns = [re_path(r'^books/$',views.Books.as_view()),re_path(r'books/(\d+)', views.Books.as_view()),
    ]
    
  • 视图

    from rest_framework.views import APIView
    # 导入序列化类
    from app01.serializers.serializer import BookSerializer
    from rest_framework.response import Response
    from app01 import  modelsclass Books(APIView):# 当请求方式为GET,且并未传入主键值时,默认返回所有图书,返回结果为JSON格式。def get(self,request,*args):back_dic = {'status': '200', 'msg': '获取成功', 'data': []}try:id = args[0]# 先查出指定获取的文章对象book_obj = models.Book.objects.filter(pk=id).first()# 实例序列化类book_ser = BookSerializer(book_obj)# book_ser.data 相当于book_obj的字典版本  (queryset转dict)back_dic['data'].append(book_ser.data)return Response(back_dic)# 如果args取不到索引值,那么说明直接访问的是books页面,返回所有的图书except IndexError:book_queryset = models.Book.objects.all()for book_obj in book_queryset:book_ser = BookSerializer(book_obj)# 将每一个book_obj都转成字典格式,塞进数据中,随后一起通过Response以JSON格式的形式发给前端。back_dic['data'].append(book_ser.data)return Response(back_dic)
    

    book_ser:BookSerializer(<Book: Book object (1)>):

    book_ser.data:{‘name’: ‘活着’, ‘price’: ‘31.00’, ‘author’: ‘余华’, ‘publish’: ‘作家出版社’}

    book_ser.data 返回Queryset对象转成dict字典格式的数据

5.1.1 补充:如何批量序列化

在上文的代码中,在处理所有Queryset对象序列化成字典的时候,是利用的for循环,其实DRF还提供了其他更方便的方法,那就是在序列化的时候就传入一个参数,告诉它需要序列化多个

参数名:

  • many=True

  • 代码示例:

    # views.py
    class BooksView(APIView):def get(self,request):response_msg = {'status': 100, 'msg': '成功'}# 获取Book表中所有的数据books=Book.objects.all()# 生成序列化对象,把需要进行序列化的对象传入,并指定序列化多条。book_ser=BookSerializer(books,many=True)  #序列化多条,如果序列化一条,不需要写# 字典新增名为data的key,值为序列化完毕的字典格式数据。response_msg['data']=book_ser.datareturn Response(response_msg)
    

5.2 PUT修改数据(反序列化)

在3.3.1章节中,介绍了如何将后端的Queryset对象转成字典,那么也需要有对应的方法,可以将前端提交过来的JSON数据,转成后端方便使用的字典。

步骤总览:

  • 依旧是书写一个序列化类,继承serializers.Serializer

  • 随后在类中写需要反序列化的字段,而这些字段要与前端提交的数据相符合,如:

    '''
    前端提交:{'name':"活着",'price':998,'author':'余华'}
    ''''''
    后端序列化类:
    '''
    class BookSerializer(serializers.Serializer):name = serializers.CharField()price = serializers.CharField()author = serializers.CharField()#publish = serializers.CharField(allow_blank=True)   #字段对应
    

    前端提交了三组键值对数据(虽然是JSON格式),那么后端就要有对应的字段做校验,后端可针对各个字段做校验限制,类似于forms组件

  • 视图函数中导入序列化类,并实例化得到对象,由于本章节是要修改数据,所以还需要把被修改的对象与参照数据一起传入,如:

    boo_ser = BookSerializer(book_obj,request.data)
    '''
    book_obj 为需要修改的对象(因为调用了修改数据接口,所以要以重新传入的数据为准)
    request.data 由于APIView对请求request又完成了一次封装,所以.data可以获取到所有编码发送的数据,包括JSON综合意思就是,实例化的时候先通过序列化类的字段普通校验,然后进行修改数据的准备工作,将需要进行修改的数据,和前端发送的更新数据,一起收集,用于后续更改。另外一种写法:   boo_ser = BookSerializer(instance=book_obj,data=request.data)
    '''
    
  • 与forms组件类似,校验完毕之后is_valid()方法可以查看是否通过,返回值为布尔,如果校验通过就调用save()保存,但是在设计的时候,这个**save()**方法需要重写。

  • 剩下的整理下各种不通过的逻辑,以及返回给前端的状态码等,最后为了符合Restful规范,修改完毕之后返回对象。

代码:

  • 路由略

  • 视图

    class Books(APIView):# 重复代码略(上面发过)def get(self,request,*args):passdef put(self, request, id):response_msg = {'status':200,'msg':'成功'}# 先获取需要修改的对象book_obj = models.Book.objects.filter(pk=id).first()# 得到一个序列化类的对象book_ser = BookSerializer(instance=book_obj,data=request.data)'''表示:后者的数据,会用于修改前者。  也就是需要拿request.data的数据 来修改book_obj的信息,request.data为前端发送过来的数据,并且不受编码的影响。也可以写成  BookSerializer(book_obj,request.data)之所以推荐写成instance=book_obj的形式,是因为后面还有个参数叫data,而这个是关键词参数,直接传的话容易成位置参数传给instance,所以只是加深下印象,以便出错。'''# 类似于forms组件一样,到我们写的BookSerializer类中做校验if book_ser.is_valid():# 保存book_ser.save()# 字段校验之后,会在重写后的update方法里进行数据修改# 修改完数据后,将修改之后的数据返回。response_msg['data'] = book_ser.dataelse:# 校验没通过就说明有字段的值是空的,或者长度等不够等,不过我们并没有设置。response_msg['status'] = 210response_msg['msg'] = '数据校验失败'response_msg['errprs'] = book_ser.errorsreturn Response(response_msg)
    

    由于前面提到了,校验通过之后,直接执行save()方法是会报错的,因为不符合设计时使用的规范,所以需要重写。

    查看到是book_ser调用的,而该对象又是通过序列化类生成的,所以直接在序列化类中进行重写。

  • 序列化类

    from rest_framework import  serializers
    from app01 import  modelsclass BookSerializer(serializers.Serializer):name = serializers.CharField()price = serializers.CharField()author = serializers.CharField()publish = serializers.CharField()def update(self, instance, validated_data):#instance是book_obj这个对象,validated_data是校验后返回的字典格式数据# 由于上传的JSON数据都是通过校验好的,那么就直接开始按照对应关系,将book_obj重新赋值,随后save保存instance.name=validated_data.get('name')instance.price=validated_data.get('price') instance.author=validated_data.get('author')instance.publish=validated_data.get('publish')'''注意,由于是instance调用的save方法,而instance又是book_obj。因此,此处的save()方法为ORM提供的。'''instance.save()  return instance
    

    instance.save()这里调用的ORM的方法,重新赋值修改之后,保存到数据库。

查看效果:

  • 接口截图来源与 Apipost软件

5.2.1 补充:partial参数

在更新的时候,可以传入partial=True,这样传入那些字段,就表示更新那些字段的值。

在不设置该参数或者read_only的时候,未传字段会报**“This field is required”**

5.3 POST新增数据(反序列化)

步骤:

  • 与PUT修改数据的步骤类似,但也有几个不同点:

    一、在实例化的时候由于并不需要修改谁,所以instance参数不需要传,但该参数在定义时又是位于前列的位置参数,所以在传入data参数的时候,要以关键字传参的形式传入进去。

    二、PUT修改中,在调用.save()方法时会报错,因为需要重写update方法,而在POST中,需要重写create方法。

# views.py
class BooksView(APIView):# 新增def post(self,request):response_msg = {'status': 100, 'msg': '成功'}#修改才有instance,新增没有instance,只有databook_ser = BookSerializer(data=request.data)# book_ser = BookSerializer(request.data)  # 这个按位置传request.data会给instance,就报错了# 校验字段if book_ser.is_valid():# 数据的新增,将在重写的create方法中进行操作,视图这里直接可以进行保存。book_ser.save()response_msg['data']=book_ser.dataelse:response_msg['status']=102response_msg['msg']='数据校验失败'response_msg['data']=book_ser.errorsreturn Response(response_msg)#ser.py 序列化类重写create方法def create(self, validated_data):# 直接利用**打散,将 A:B 的字典形式,转换成 A=B# 创建数据,最后需要把ORM创建返回的对象返回。instance=Book.objects.create(**validated_data)return instance# urls.py
path('books/', views.BooksView.as_view()),

5.4 删除一个数据(不需要序列化)

删除就不需要使用到序列化了,只是为了凑出增删改查四兄弟的。

# views.py
class BookView(APIView):def delete(self,request,pk):ret=Book.objects.filter(pk=pk).delete()return Response({'status':100,'msg':'删除成功'})
# urls.py
re_path('books/(?P<pk>\d+)', views.BookView.as_view()),

5.5 序列化类的字段类型

BooleanField BooleanField()
NullBooleanField NullBooleanField()
CharField CharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True)
EmailField EmailField(max_length=None, min_length=None, allow_blank=False)
RegexField RegexField(regex, max_length=None, min_length=None, allow_blank=False)
SlugField SlugField(maxlength=50, min_length=None, allow_blank=False) 正则字段,验证正则模式 [a-zA-Z0-9-]+
URLField URLField(max_length=200, min_length=None, allow_blank=False)
UUIDField UUIDField(format=’hex_verbose’) format: 1) 'hex_verbose'"5ce0e9a5-5ffa-654b-cee0-1238041fb31a" 2) 'hex'"5ce0e9a55ffa654bcee01238041fb31a" 3)'int' - 如: "123456789012312313134124512351145145114" 4)'urn' 如: "urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"
IPAddressField IPAddressField(protocol=’both’, unpack_ipv4=False, **options)
IntegerField IntegerField(max_value=None, min_value=None)
FloatField FloatField(max_value=None, min_value=None)
DecimalField DecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None) max_digits: 最多位数 decimal_palces: 小数点位置
DateTimeField DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)
DateField DateField(format=api_settings.DATE_FORMAT, input_formats=None)
TimeField TimeField(format=api_settings.TIME_FORMAT, input_formats=None)
DurationField DurationField()
ChoiceField ChoiceField(choices) choices与Django的用法相同
MultipleChoiceField MultipleChoiceField(choices)
FileField FileField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)
ImageField ImageField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)
ListField ListField(child=, min_length=None, max_length=None)
DictField DictField(child=)

5.6 序列化类的字段参数

选项参数:

参数名称 作用
max_length 最大长度
min_lenght 最小长度
allow_blank 是否允许为空
trim_whitespace 是否截断空白字符
max_value 最小值
min_value 最大值

通用参数:

参数名称 说明
read_only 表明该字段仅用于序列化输出,默认False
write_only 表明该字段仅用于反序列化输入,默认False
required 表明该字段在反序列化时必须输入,默认True
default 反序列化时使用的默认值
allow_null 表明该字段是否允许传入None,默认False
validators 该字段使用的验证器
error_messages 包含错误编号与错误信息的字典
label 用于HTML展示API页面时,显示的字段名称
help_text 用于HTML展示API页面时,显示的字段帮助提示信息

5.6.1 read_only和write_only

read_only:

  • 表明该字段仅用于序列化输出,默认False,如果设置成True,postman中可以看到该字段,修改时,不需要传该字段。

write_only:

  • 表明该字段仅用于反序列化输入,默认False,如果设置成True,postman中看不到该字段,修改时,该字段需要传

测试:

class BookSerializer(serializers.Serializer):id = serializers.CharField(read_only=True)name = serializers.CharField()price = serializers.CharField(write_only=True)#author = serializers.CharField()author = serializers.CharField(validators=[check_author])publish = serializers.CharField()

以下的了解即可:

  • required 表明该字段在反序列化时必须输入,默认True
  • default 反序列化时使用的默认值
  • allow_null 表明该字段是否允许传入None,默认False
  • validators 该字段使用的验证器
  • error_messages 包含错误编号与错误信息的字典

5.6.2 继承ModelSerializer的只读只写

在六章节中,自定义序列化类继承ModelSerializer之后,由于不再一个个书写需要序列化的字段,那么read_only和write_only与设置方式就会有所不同。

代码示例:

# 注:需要结合6.1章节及以后内容
class BookSer(ModelSerializer):def __init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.fields['bind_publish'].write_only = Trueself.fields['bind_author'].write_only = Trueclass Meta:model = models.Bookfields = ['title','price','publish_name','author_list','bind_publish','bind_author']extra_kwargs={'publish_name':{'read_only':True},'author_list':{'read_only':True},}
  • extra_kwargs中只能书写read_only,写write_only还是会报错。

由于只读和只写不能同时出现在extra_kwargs中,且经过测试发现write_only写在extra_kwargs还是会报错,因此只能写在调用父类init方法的里面。

报错:AssertionError: May not set both read_only and write_only

5.7 钩子函数

5.6章节中介绍了很多用来校验的参数,但是如果没有自己想要的该怎办呢? 钩子函数,以及字段的validators参数

局部钩子:

  • 用于单字段值的校验,想要针对那个字段的值进行校验,就创建validate_字段名的函数,该函数就是局部钩子函数,形参data就是前端发送的该字段值,后续可自行进行更复杂的校验等操作,不符合的需要抛出ValidationError

全局钩子:

  • 用于多字段值的校验
from rest_framework import  serializers
# 导入抛出异常的模块
from rest_framework.validators import ValidationErrorclass BookSerializer(serializers.Serializer):'''略'''def update(self, instance, validated_data):'''略'''return instance# 局部钩子,validate_字段名,  data用于接收前端提交的,price字段相关的JSON数据def validate_price(self, data):   #如果价格大于1000就抛出异常if float(data)<1000:return dataelse:raise ValidationError('你这书保熟吗')# 全局钩子def validate(self, data):    #由于多个字段的值都在这,所以数据格式为字典,需要get取值name = data.get('name')author = data.get('author')if author == name:raise ValidationError('作者名与书名不能相同')else:return data

validators参数

  • 可以使用字段的author=serializers.CharField(validators=[check_author]) ,来校验
def check_author(data):if 'sb' in data:raise ValidationError('名称不雅')else:return dataclass BookSerializer(serializers.Serializer):name = serializers.CharField()price = serializers.CharField()author = serializers.CharField(validators=[check_author])publish = serializers.CharField()

5.8 自己封装Response对象

在前面的代码中,出现了大量的重复代码,例如下列的代码:

back_dic = {'status': '200', 'msg': '获取成功', 'data': []}

思路:

class MyResponse():def __init__(self):self.status = 100self.msg = '成功'@propertydef get_dict(self):return self.__dict__res = MyResponse()
res.status = 101
res.msg = '查询失败'
# 需要添加返回信息的时候,可以直接.data来给对象添加属性
# res.data={'name':'liuyu'}
print(res.get_dict)
# {'status': 101, 'msg': '查询失败','data':{'name':'liuyu'}}
  • 使用的时候直接实例化对象,然后通过对象来操作属性及值,最后通过开设的get_dict接口,访问到该对象所有的属性和方法。

    @property : 将对象的方法,伪装成属性(调用不需要加括号)

    __dict__ : 获取对象所有的属性和方法

5.9 Serializer高级用法

source参数

  • 可以改字段名字

    xxx=serializers.CharField(source='title')
    

  • 返回给前端的数据,可以“.”跨表

    publish=serializers.CharField(source='publish.email')
    

    开头和参数内可以理解为隐藏了个表,以案例的图书表为例,这段代码就变成了下列这种

    book.publish=serializers.CharField(source='book.publish.email')
    

    此时的publish为book表的外键字段,最终返回给前端的JSON数据为 {‘publish obj’ : ’ 123@qq.com '}

    所以,在模型表的publish类中,最好定义双下str方法,这样key就变成了中文,而不是obj对象,看不出来是什么。

serializers.SerializerMethodField()

  • 应用场景:当需要把多个值,序列化到一个key中的时候,如:

    {'name':'liuyu','age':22,'hobby':[{'type':'美食','detailed':'吃'},{'type':'娱乐','detailed':'打游戏'}]
    }
    

    代码示例:

    class BookSerializer(serializers.Serializer):name=serializers.CharField()price=serializers.CharField()publish=serializers.CharField()authors=serializers.SerializerMethodField() #SerializerMethodField需要有个配套方法,方法名叫get_字段名,返回值就是前端要显示的数据def get_authors(self,instance):# instance:book对象authors_queryset = instance.authors.all()  # 取出所有作者back_dic=[]for author_obj in authors_queryset:back_dic.append({'name':author_obj.name,'age':author_obj.age})return back_dic
    

    以图书表为例,views.py在给前端返回JSON数据的时候,先生成Serializer对象,然后这个过程中完成序列化,authors字段在序列化的时候会执行下面的get_authors方法,命名格式就是get_字段名,注意需要传入instance参数,该参数的值,就是views.py中调用序列化类时所传入的book_queryset对象,有个图书对象以后,自然就可以直接进行跨表查询,随后封装下返回值。

1

六、ModelSerializer

ModelSerializer模型类序列化器与序列化器的区别:

  • 序列化类父类继承不一样

    # 这是序列化器,也就是五章节中用的
    class BookSerializer(serializers.Serializer):pass代码略# 这是模型类序列化器,将在六章介绍介绍使用
    from rest_framework.serializers import ModelSerializer
    class BookModelSerializer(serializers.ModelSerializer):
    

特点一:序列化类不需要一个一个写字段

from app01 import modelsclass BookModelSerializer(serializers.ModelSerializer):class Meta:model= models.Book  # 对应上models.py中的模型fields='__all__'# 只读只写在这里配置extra_kwargs = {  'price': {'write_only': True},}
  • 直接fields=‘__all__’即可展示所有的字段,如果想要指定某个字段只读或只写,由于不再书写字段,就在extra_kwargs参数中定义即可

特点二:修改和新增数据时,不需要重写update方法和create方法

  • 不需要再重写update方法和create方法

6.1 fields属性

在上面的代码中,直接fields=‘__all__’来展示所有字段,一些不希望展示的直接设置只读只写来控制,那么除了这么写以外,还有其他的写法。

将展示的字段,以元祖或列表的形式来展示。

class BookSer(ModelSerializer):class Meta:model = models.Bookfields = ('title','price',)#fields = ['title','price']  #也可以是列表

以上图代码为例,title与price对应的就是models.py中book类的几个属性,那么也就以为着,fields可以传入函数

# ser.py
class BookSer(ModelSerializer):class Meta:model = models.Bookfields = ['title','price','publish_name','author_list']  # 序列化models.Book类中title属性、price属性、....的值,# models.py
class Book(models.Model):title = models.CharField(max_length=32)price = models.DecimalField(max_digits=8,decimal_places=2)# 与出版社的外键bind_publish = models.ForeignKey(to='Publish',on_delete=models.DO_NOTHING,db_constraint=False)# 与作者表的外键bind_author = models.ManyToManyField(to='Author',through='Book2Author',through_fields=('bind_book','bind_author'))@propertydef publish_name(self):return self.bind_publish.name@propertydef author_list(self):author_list=self.bind_author.all()# 返回对应的用户列表,该列表内的数据为对应作者的姓名和性别。return [ {'name':author.name,'sex':author.get_sex_display()}  for author in author_list]

效果展示:

  • 可以做到返回跨表查询过来的数据。

七、Request与Response

在五章节中,有用到例如.data方法,本章节单独拎出来做总结。

7.1 请求

源码分析见第四章

from rest_framework.request import Request
'''
其实就是将request进行了二次封装,将原生django request作为drf request对象的 _request 属性
'''

封装之后的request,常用的两个方法:

  • 请求对象.data:前端以三种编码方式传入的数据,都可以取出来。
  • 请求对象.query_params:与Django标准的request.GET相同,只是更换了更应景的名称而已。

7.2 响应

from rest_framework.response import Response

response方法,在上文中的时候,都是传入request.data,但该方法是可以传入多个参数的。

reponse方法,各个参数的作用:

def __init__(self, data=None, status=None,template_name=None, headers=None,exception=False, content_type=None):
  • data:要返回的数据,字典

  • status:返回的状态码,默认是200

    return response(requese.data,status:201)
    

    drf在status模块中,定义了很多常量,常量对应的有状态码,所以可以导入,在status参数这里,返回更为专业一点的状态码

    from rest_framework import statusreturn response(requese.data,status:status.HTTP_200_OK)
    
  • template_name: 渲染的模板名字(自定制模板),不需要了解。

  • headers:响应头,可以往响应头放东西,格式为字典

  • content_type:响应的编码格式,application/json和text/html

Response在响应浏览器和接口测试工具的时候,响应的格式是不一样的。

  • 浏览器访问时,会返回给前端模板。
  • 接口测试工具访问时,返回的就是JSON数据。

如何设置不管是什么访问,都只返回JSON数据呢:

全局配置:

  • #全局使用:全局的视图类,所有请求,都有效
    #在setting.py中加入如下
    REST_FRAMEWORK = {'DEFAULT_RENDERER_CLASSES': (  # 默认响应渲染类'rest_framework.renderers.JSONRenderer',  # json渲染器'rest_framework.renderers.BrowsableAPIRenderer',  # 浏览器API渲染器)
    }
    

    由于JSON数据肯定是要返回的,所以当我们不想要浏览器返回模板的时候,就在django项目文件夹下的settings.py中,将上述代码拷贝进去,把BrowsableAPI这段注释掉就行。

局部配置:

  • # 对某个视图类有效
    # 在视图类中写如下from rest_framework.renderers import JSONRenderer
    # 该视图函数对应的所有请求,都返回JSON格式,不会浏览器进行渲染。
    class BooksView(APIView):renderer_classes=[JSONRenderer,]
    

    drf框架也有static文件夹、templates文件夹,还有一个api.html 这也就是为什么,drf没在django里注册,浏览器访问接口的时候,会报错的原因。

    drf的settings文件中,有着大量的配置,类似于注册中间件一样,所以可以利用drf设定的查找顺序,在django项目的settings中进行修改。

    drf有默认的配置文件—》先从项目的setting中找,找不到,采用默认的,而默认的REST_FRAMEWORK属性中,两个响应渲染类模块都是启用的。

八、视图

APIView是REST framework提供的所有视图的基类,继承自Django的View父类。

APIView与View的不同之处在于:

  • 传入到视图方法中的是REST framework的Request对象,而不是Django的HttpRequeset对象;
  • 视图方法可以返回REST frameworkResponse对象,视图会为响应数据设置(render)符合前端要求的格式;
  • 任何APIException异常都会被捕获到,并且处理成合适的响应信息;
  • 在进行dispatch()分发前,会对请求进行身份认证、权限检查、流量控制。

8.1 基于APIView写接口

代码与前面章节基本一致,再次提及主要是用于对比其他模块。

# views.py
from rest_framework.generics import GenericAPIView
from app01.models import Book
from app01.ser import BookSerializer#models.py
class Book(models.Model):name=models.CharField(max_length=32)price=models.DecimalField(max_digits=5,decimal_places=2)publish=models.CharField(max_length=32)# 基于APIView写的
class BookView(APIView):def get(self,request):book_list=Book.objects.all()book_ser=BookSerializer(book_list,many=True)return Response(book_ser.data)def post(self,request):book_ser = BookSerializer(data=request.data)if book_ser.is_valid():book_ser.save()return Response(book_ser.data)else:return Response({'status':101,'msg':'校验失败'})class BookDetailView(APIView):def get(self, request,pk):book = Book.objects.all().filter(pk=pk).first()book_ser = BookSerializer(book)return Response(book_ser.data)def put(self, request,pk):book = Book.objects.all().filter(pk=pk).first()book_ser = BookSerializer(instance=book,data=request.data)if book_ser.is_valid():book_ser.save()return Response(book_ser.data)else:return Response({'status': 101, 'msg': '校验失败'})def delete(self,request,pk):ret=Book.objects.filter(pk=pk).delete()return Response({'status': 100, 'msg': '删除成功'})#ser.py
class BookSerializer(serializers.ModelSerializer):class Meta:model=Bookfields='__all__'
# urls.py
path('books/', views.BookView.as_view()),
re_path('books/(?P<pk>\d+)', views.BookDetailView.as_view()),

1、BookView用于处理get与post请求,应为不需要传入主键值,BookDetailView用于处理Put、delete、get(单条数据)请求。

2、实例化传入值,由于序列化类使用的是ModelSerializer,所以不需要写上一大堆想要序列化的字段,直接内部顶一个名为Meta的类即可。

3、当序列化多条数据时,需要传入many=True

基于APIView写接口的代码量就这么多,接下来看看在使用其他封装程度更高的接口,能带来什么样的效果。

8.2 基于GenericAPIView写的接口

8.1中,视图类继承的是APIView,本章节将介绍是怎么基于GenericAPIView写接口的。

序列化类:

  • from rest_framework import serializers
    from app01.models import Book
    class BookSerializer(serializers.ModelSerializer):class Meta:model=Bookfields='__all__'
    

视图层:

  • from rest_framework.generics import GenericAPIViewclass BookView(GenericAPIView):queryset = Book.objects    #queryset要传queryset对象,查询了所有的图书。也可以写成queryset=Book.objects.all()serializer_class = BookSerializer    #使用哪个序列化类来序列化这堆数据def get(self,request):book_list=self.get_queryset()book_ser=self.get_serializer(book_list,many=True)return Response(book_ser.data)def post(self,request):book_ser = self.get_serializer(data=request.data)if book_ser.is_valid():book_ser.save()return Response(book_ser.data)else:return Response({'status':101,'msg':'校验失败'})class BookDetailView(GenericAPIView):queryset = Book.objectsserializer_class = BookSerializerdef get(self, request,pk):book = self.get_object()book_ser = self.get_serializer(book)return Response(book_ser.data)def put(self, request,pk):book = self.get_object()book_ser = self.get_serializer(instance=book,data=request.data)if book_ser.is_valid():book_ser.save()return Response(book_ser.data)else:return Response({'status': 101, 'msg': '校验失败'})def delete(self,request,pk):ret=self.get_object().delete()return Response({'status': 100, 'msg': '删除成功'})
    

路由:

  • path('books/', views.BookView.as_view()),
    re_path('books/(?P<pk>\d+)', views.BookDetailView.as_view()),
    

可以发现,继承了GenericAPIView之后,在写这五个接口时,只需要在类中定义queryset属性和serializer_class属性,后面每个接口的代码基本上都是大差不差,并且后续的代码中,我们并没有书写ORM查询,只是单纯的通过封装好的get_serializer方法来调用序列化器,并传入datainstance等参数并save即可。

针对于PUT修改、指定GET、DELETE删除,在继承APIView的时候,还需要书写ORM查询语句,filter(pk=id),但是到了这里,直接self.get_object()

并且,如果要对出版社表和作者表,也对外开放增删改查接口时,可以整体拷贝图书表的代码,只需要重新写一个序列化类,然后在视图层修改下queryset、serializer_class属性的值即可。

8.3 GenericAPIView与5个视图扩展类

在8.2章节中,利用了再次封装了APIView的GenericAPIView,从而实现了减少重复代码的作用,但目前仍存在很多重复代码,所以drf又提供了五个视图扩展类。

ListModelMixin: list方法,可以直接获取到所有数据

CreateModelMixin: create方法,可以直接创建数据

UpdateModelMixin: update方法,修改数据

DestroyModelMixin:destroy方法,删除数据。

RetrieveModelMixin:retrieve方法,可以查询到指定主键的数据

使用示例:

from rest_framework.mixins import ListModelMixin,CreateModelMixin,UpdateModelMixin,DestroyModelMixin,RetrieveModelMixin
# views.py
class BookView(GenericAPIView,ListModelMixin,CreateModelMixin):queryset=Book.objectsserializer_class = BookSerializerdef get(self,request):return self.list(request)def post(self,request):return self.create(request)class BookDetailView(GenericAPIView,RetrieveModelMixin,DestroyModelMixin,UpdateModelMixin):queryset = Book.objectsserializer_class = BookSerializerdef get(self, request,pk):return self.retrieve(request,pk)def put(self, request,pk):return self.update(request,pk)def delete(self,request,pk):return self.destroy(request,pk)# urls.pypath('books/', views.BookView.as_view()),re_path('books/(?P<pk>\d+)', views.BookDetailView.as_view()),

注意:

  • 路由的有名分组,和视图中get、put等方法中,必须叫pk,都改成id则会报错

    报错:AssertionError: Expected view BookDetailTestView to be called with a URL keyword argument named “pk”. Fix your URL conf, or set the .lookup_field attribute on the view correctly.

8.3.1 分析

请求来的时候,会被APIView进行封装,如果是get获取全部数据,那么会经过GenericAPIView的ListModelMixin扩展类,可调用该扩展类内部的list方法进行ORM查询,以及序列化返回给前端。 数据的来源以及序列化库中的哪些字段,这些由query和serializer_class属性来决定,而这二者的值由我们来传。

如果来得是post请求,同样会被APIView进行封装,提交的数据在request.data内,从中取出并先进行序列化校验,如果继承的不是APIView那么就指名道姓的调用该类来实例化对象(传入参数时需要注意data=),如果该视图类继承的是GenericAPIView,那么就需要serializer_class参数来决定由哪个序列化器进行校验。

校验完毕之后就是保存到数据,此时如果序列化类继承的是Serializer而不是ModelSerializer时,还需要重写create方法,新建数据的操作就是在这create方法中进行的,创建完毕之后return instance,随后把用户提交的数据,再**.data**序列化返回给前端,因为既然数据都创建出来了,那么用户输入的数据肯定没问题,没问题那就扭头告诉一声说创建好了,name叫什么 age是多少…正符合RESTful规范。

put、delete与指定主键get,这三个都需要在URL中以路径的形式,来传入主键值,从而确定用户想要哪条数据、想改哪条数据、想删哪条数据。

  • 先说get,请求来了之后…略…,需要先查询数据,把用户想要的那一条查出来,然后对查出来的数据进行序列化,最后Response返回,但使用了GenericAPIView之后,ORM的查询不需要写了,调用.get_object()即可获取到,随后再调用.get_serializer()传入需要实例化的对象完成序列化,此时格式为字典,最后Response处理成JSON字符串返回出去。

    如果不仅仅用了GenericAPIView,还用了ListModelMixin扩展类,那么直接self.list(request)即可(需要在视图了中指定两个参数)

    get_object() 与 get_serializer()

    • 就是获取我们视图类中一开始就定义的queryset和serializer_class这两个属性的值

      前者用于指定序列化的数据,后者指定用于序列化采用的序列化类

  • 再说put,和上面的post新增是一样的,如果序列化类继承的是Serializer,那么在修改数据的时候,需要重写序列化类中的update方法,然后调用ORM的save()保存,继承ModelSerializer时就不需要这样,这是序列化类的层面。

    视图类这里,如果继承的是GenericAPIView那么就不需要我们写ORM语句,但是其他逻辑代码与基于APIView的接口代码,几乎是一样的。 如果再继承了UpdateModelMixin扩展类,那么就可以直接调用内部封装的update方法,直接完成更新,不需要再书写其他什么调用序列化类、序列化校验是否合法等等,直接return self.update(request,pk)

8.4 GenericAPIView的9个视图子类

总结:

  • 在基于GenericAPIView写接口的时候,可以发现我们少写了一些查询。

    然后再利用GenericAPIView+5个扩展类,可以实现每个方法的代码进一步缩减,直接调用针对该请求封装好的方法即可。

  • 目前我们的代码,两个视图类BookView与BookDetailView中,还是书写了get、post、put、delete请求方法,本章节可以实现的就是,可以连这些方法都不需要写。

    9个视图子类,可以做到识别到请求方式,然后执行对应的5个扩展类方法,封装程度更高更方便。

代码如下:

  • 这9个视图子类都继承了GenericAPIView和xxxModelMixin扩展类,所以直接继承就好。

    from rest_framework.generics import CreateAPIView,ListAPIView,UpdateAPIView,RetrieveAPIView,DestroyAPIViewclass BookView(ListAPIView,CreateAPIView):  queryset = Book.objectsserializer_class = BookSerializerclass BookDetailView(UpdateAPIView,RetrieveAPIView,DestroyAPIView):queryset = Book.objectsserializer_class = BookSerializer
    

代码解析:

  • BookView视图类继承了 ListAPIView 和 CreateAPIView,而这两个类分别又继承了GenericAPIView + ListModelMixin扩展类、GenericAPIView + CreateModelMixin 扩展类。

  • ListAPIView类中定义了get函数,并且调用了ListModelMixin中的list方法,CreateAPIView类中定义了post函数,并且调用了CreateModelMixin中的create方法。

  • 所以,串起来就是,当访问来时候,先进行路由匹配,成功之后执行视图类中的函数as_view函数,最后dispatch的时候,get方法post方法都在 ListAPIView 和 CreateAPIView中定义的,所以自然就去执行框架封装好的代码来响应请求。

BookView用于处理get全部和post新增,内部的get方法和post方法,都已经封装到了ListAPIView与CreateAPIView中。

BookDetailView这个类是处理put修改、get单个查询、删除操作,原本def put、def get…这些定义请求类型的视图函数,全部都被封装到了UpdateAPIView、RetrieveAPIView、DestroyAPIView中。

  • 所以只需要书写GenericAPIView需要给值的两个参数即可。(因为这9个类,都是GenericAPIView的子类)

如果我这个视图类,只处理get和post请求,不接受其他的请求,那么只需要继承对应三个类即可。

同时为了更方便,ListAPIView与CreateAPIView还可以再合并,合并为ListCreateAPIView

另外三个组合为:(加起来就是9个)

  • RetrieveUpdateDestroyAPIView get单个、更新、删除
  • RetrieveDestroyAPIView get单个、删除
  • RetrieveUpdateAPIView get单个、更新

8.5 ModelViewSet

在8.4中,此时的代码已经很少了,但是在处理这五个请求的时候,还是拆成了两个视图类来处理的,因为有几个需要传入主键值,虽然我们可以自己去解决这个问题,但没那么方便了。drf提供的ModelViewSet,就可帮我们解决这个问题。

使用方法:

  • # 视图
    from rest_framework.viewsets import ModelViewSet
    class Book5View(ModelViewSet):  #5个接口都有queryset = Book.objectsserializer_class = BookSerializer
    
    urlpatterns = [# 两个路由都对应一个视图函数path('books5/', views.BookView.as_view(actions={'get':'list','post':'create'})), re_path('books5/(?P<pk>\d+)', views.BookView.as_view(actions={'get':'retrieve','put':'update','delete':'destroy'})),
    ]
    

    actions参数后面的值是什么意思呢?

    • 当第一条路径匹配中了之后,如果是get请求,那么就找到GenericAPIVIew视图扩展类之一ListModelMixin中的list方法并执行。
    • 当第二条路径匹配中了之后,如果是get请求,那么就找到GenericAPIVIew视图扩展类之一RetrieveModelMixin中的retrieve方法并执行。
    • 以此类推。

路由其实也可以自动生成。详情见九章节。

8.5.1 ViewSetMixin改写路由

上文中actions改写路由的方法,不只局限于ModelViewSet,ViewSetMixin也可以改写路由

# views.py
from rest_framework.viewsets import ViewSetMixin
class BookView(ViewSetMixin,APIView):    #一定要放在APIVIew前def get_all_book(self,request):print("xxxx")book_list = Book.objects.all()book_ser = BookSerializer(book_list, many=True)return Response(book_ser.data)# urls.py#继承ViewSetMixin的视图类,路由可以改写成这样path('books/', views.BookView.as_view(actions={'get': 'get_all_book'})),

可以利用ViewSetMixin来实现,控制路由匹配,当匹配中了且访问是get请求,那么就交给BookView类中的get_all_book函数处理。

需要注意的是,在继承的时候ViewSetMixin一定要在APIView的前面,因为二者都拥有as_view和view方法,面向对象继承的属性查找顺序是:自己没有就先去第一个父类中查找,所以一定要放在前面。

ViewSet:

  • 继承了ViewSetMixin和APIView,也可以直接使用ViewSet,效果是一模一样。

九、路由

在8.5章节中,我们使用ModelViewSet和ViewSetMixin,可以实现路由改写这么一个效果,当路由匹配中了之后,根据请求的方式,来分别设定响应所要执行的方法,如:

path('books/', views.BookView.as_view(actions={'get':'list','post':'create'})),
#当路径匹配,又是get请求,会执行BookView的list方法 (list来源于ListModelMixin)

drf中,可以自动生成路由,不需要我们再去使用什么re_path写正则,或者使用转换器什么的。

使用之前需要注意,视图类需要继承ModelViewSet

routers模块:

  • 第一步:导入routers模块。

    第二步:有两个类,实例化得到对象。

    # urls.py
    from rest_framework import routers
    routers.DefaultRouter
    routers.SimpleRouter
    '''
    DefaultRouter 与  SimpleRouter   二选一
    前者可以生成更多路由,但是没什么用,一般都是后者SimpleRouter简单路由
    '''
    
  • 第三步:注册

    # router.register('前缀','继承自ModelViewSet视图类','别名')    别名可用作反向解析
    router.register('books',views.BookViewSet)
    
  • 第四步:将自动生成的路由,加入到原路由中

    urlpatterns += router.urls
    # 两个列表直接相加,列表相加等于拼接,当然也可以用for然后append
    

9.1 action的使用

action 的作用:

  • 给继承自ModelViewSet的视图类中定义的函数,也添加路由,承接8.5.1章节,不走GenericAPIView的扩展类。

使用示例:

  • from rest_framework.decorators import action   # 装饰器class BookViewSet(ModelViewSet):queryset =Book.objects.all()serializer_class = BookSerializer@action(methods=['GET','POST'],detail=True)def get_1(self,request,pk):# print(pk)book=self.get_queryset()[:2]  # 从0开始截取一条ser=self.get_serializer(book,many=True)return Response(ser.data)
    

    detail:该参数用来指定,此查询是否需要根据主键值进行精准查询,如果为True,那么下面被装饰的函数,就需要将pk传入。

十、认证

在我们写接口的时候,有的需要登陆之后才可以查看,比如淘宝的首页就不需要登陆,但是添加购物车就需要登陆之后才可以,所以本章节就是介绍DRF模块中的认证,以及如何自定义书写认证功能。

认证的写法:

  • 书写一个类,继承BaseAuthentication,随后重写authenticate方法,认证的逻辑就写在该方法内。

    认证通过返回两个值,其中一个值最终给了Requet对象的user。

    认证失败,抛异常:APIException或者AuthenticationFailed

  • 与序列化校验组件一样,认证也是分为全局配置局部配置的。

认证的源码分析:

  • 前面的APIView源码分析汇总有提到,APIView中重新定义了as_view,然后绕了一圈又回到了自己的dispatch,dispatch方法中又调用了initial,而这里面有认证、权限、频率的校验。

  • 阅读initial中,认证组件相关的代码:self.perform_authentication(request)

    def perform_authentication(self, request):request.user
    

    可以发现源码里就一句request.user,这个request是经过APIView封装过的,(注:已经不是auth模块章节了,这里的.user不要记混淆)我们接着按照属性查找顺序,查找drf中的user方法,由于request其实现在是Request,所以直接去drf下的Request模块中查找user方法,并阅读下。

    核心就在self._authenticate()

    @property
    def user(self):if not hasattr(self, '_user'):with wrap_attributeerrors():self._authenticate()
    return self._user
    
    def _authenticate(self):# 遍历拿到一个个认证器,进行认证# self.authenticators 配置的一堆认证类产生的认证类对象组成的 # self.authenticators 在视图类中配置的一个个的认证类:authentication_classes=[认证类1,认证类2]for authenticator in self.authenticators:try:# 认证器(对象)调用认证方法authenticate(认证类对象self, request请求对象)# 返回值:登陆的用户与认证的信息组成的,tuple元祖# 该方法被try包裹,代表该方法会抛异常,抛异常就代表认证失败user_auth_tuple = authenticator.authenticate(self) #这里的self是request对象except exceptions.APIException:self._not_authenticated()raise# 返回值的处理if user_auth_tuple is not None:self._authenticator = authenticator# 如何有返回值,就将 “登陆用户” 与 “登陆认证” 分别保存到 request.user、request.authself.user, self.auth = user_auth_tuplereturn# 如果返回值user_auth_tuple为空,代表认证通过,但是没有 登陆用户 与 登陆认证信息,代表游客self._not_authenticated()
    

10.1 认证组件的使用

实现效果:

  • 调用图书接口时需要先登录,只有登陆之后才可以调用。

一、准备工作

由于涉及到保存用户信息,所以这里在原来图书表上,又新增了一些表。

class User(models.Model):username=models.CharField(max_length=32)password=models.CharField(max_length=32)user_type=models.IntegerField(choices=((1,'超级用户'),(2,'普通用户'),(3,'注销用户')))class UserToken(models.Model):token=models.CharField(max_length=64)user=models.OneToOneField(to=User,on_delete=models.CASCADE)
  • UserToken表用来储存给用户返回的随机字符串。

    由于在django2.x及以上版本中,默认不在级联更新/级联删除,所以在创建外键字段的时候,需要加上on_delete参数。

    on_delete参数参数的值,除了models.CASCADE级联更新删除,还有其他的,具体会在10.2章节补充。

由于需要登陆做认证,那么就先完成一个登陆接口。

  • 路由

    urlpatterns = [...path('login/', views.LoginView.as_view()),...
    ]
    
  • 视图

    from rest_framework.views import APIView
    from rest_framework.response import Response
    import uuidclass LoginView(APIView):authentication_classes = []# 获取提交的登陆数据def post(self,request):username=request.data.get('username')password=request.data.get('password')user=models.User.objects.filter(username=username,password=password).first()if user:# 登陆成功,生成一个随机字符串token=uuid.uuid4()# models.UserToken.objects.create(token=token,user=user) 用它每次登陆都会重新记录一条token,不太好。# update_or_create 有就更新,没有就新增models.UserToken.objects.update_or_create(defaults={'token':token},user=user)return Response({'status':100,'msg':'登陆成功','token':token})else:return Response({'status': 101, 'msg': '用户名或密码错误'})
    

    一、update_or_create:如果当前有数据,那么就更新覆盖,如果原来没这数据,那就创建。 参数的含义为:要更新或创建什么字段的数据,并且判断的依据是什么。

    结合起来就是,判断usertoken表中,user字段的值是否已经存在,如果没有那就创建,如果有,那么就更新usertoken表中token字段的数据。

    update_or_create(defaults={‘token’:token},user=user)

    • 前面的user表示usertoken表中的user字段
    • 后面的user表示登陆用户的对象

    在usertoken表中对应user字段存的就是对象,所以user字段的值 = 对象 = 对象 = user,如果usertoken表中已经存在该数据,那么就更新,反之就新建,继续存入这个对象以及生成的UUID。

    二、uuid:随机字符串模块,**uuid.uuid4()**可以生成一串几乎不会有重复的字符串,但也是有风险的,会出现生成UUID重复的可能,本案例只是为了看出效果。

    三、其他:

    当登陆接口的逻辑写完之后,返回JSON数据,这里使用APIView的Response还是JSONresponse,都无所谓,因为这两个方法都是将字段序列化为JSON字符串然后返回的。(这个时候还没用到认证组件)

二、书写认证类

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app01.models import UserTokenclass MyAuthentication(BaseAuthentication):def authenticate(self, request):# 认证逻辑,如果认证通过,返回两个值# 如果认证失败,抛出AuthenticationFailed异常token=request.GET.get('token')  # token值需要调用登陆API获取,如果没有那就是没登陆。if  token:user_token=UserToken.objects.filter(token=token).first()# 认证通过if user_token:return user_token.user, tokenelse:raise AuthenticationFailed('认证失败')else:raise AuthenticationFailed('请求地址中需要携带token')
  • 在认证类中,我们规定了需要在URL地址栏中,添加参数token,值为登陆之后返回给前端的token。

  • 如果认证成功,那么就返回两个值。 如果不成功那么就抛出异常,推荐是AuthenticationFailed

    其中,返回的两个值,会被request.user和request.auth所接收,前者后续需要在权限器中使用,来校验当前用户是否有权限,所以返回的第一个值需要是该用户对象。

三、使用认证类

  • 局部使用:

    class BookViewSet(ModelViewSet):# 可以有多个认证,从左到右依次执行authentication_classes=[MyAuthentication]queryset =Book.objects.all()serializer_class = BookSerializer@action(methods=['GET',],detail=True)def get(self,request,pk):...
    

    当路由匹配成功之后,如果是GET请求,那么就执行视图类中的get方法。

    现在又局部使用了认证,并且指定的认证类MyAuthentication,该类就是刚刚写的认证类,里面规定了需要以get请求的方式,传入token值做校验,至于说是放在哪里,是请求头还是浏览器缓存,这个是前端需要做的(前后端分离)。

    如果没有携带token,那么就会根据我们书写的MyAuthentication抛出异常。

    访问示例:http://127.0.0.1/book/?token=dce869ce-cac9-49ea-8d30-dc9d35b2163d get请求

  • 全局使用

    # 全局使用,在setting.py中配置
    REST_FRAMEWORK={"DEFAULT_AUTHENTICATION_CLASSES":["app01.app_auth.MyAuthentication",]
    }# 附:局部禁用
    authentication_classes=[]
    

    注意:一旦使用全局,那么连登陆也需要校验,所以就要再使用局部禁用。

至此,当调用图书接口的时候,由于我们书写了自定义认证,需要在URL后面跟上token参数,并且根据值进行校验,而这个token值只有登陆之后才会生成,所以这就完成了接口的认证。

10.2 ORM补充

2023-2-7,补充下ORM章节遗漏

10.2.1 on_delete参数

该参数表示级联关系:

  • models.CASCADE 级联更新删除(作者没了,详情也没)

  • models.DO_NOTHING 原封不动(出版社没了,书还是那个出版社出版)

  • models.SET_NULL 删除设置为空(部门没了,员工部门为空,但是该字段要有null=True)

  • models.SET_DEFAULT 删除设置为默认值(员工进入默认部门)

10.2.2 断关联

绑定了外键关系以后,在插入数据的时候会因为外键值当前并不存在而报错,如果需要先把数据新增,然后再完善外键关系时,就需要使用到断关联,断开实质上的外键关系。

断开以后就是逻辑上的关联,实质上没有外键联系,增删不会受外键影响,而且还可以提升效率。

db_constraint=False

在没有断开关联的时候,插入一条数据时,如果外键绑定的那条数据并不存在,那么会插入不进去。

  • 如:新增图书,绑定出版社外键为1000的,但是id=1000的出版社并不存在,那么这个时候数据会插入不进去。

    断开关联就可以直接插入进去,删除也是同理,删除作家出版社,如果绑定的有图书是产出不掉的。

需要注意脏数据,由于不去根据外键进行校验,所以图书很可能会绑定上并不存在的出版社或作者,这种数据就叫做脏数据,那么作为程序员就需要避免脏数据的产生。

10.2.3 抽象表

在实际开发当中,每张表基本上都要有的字段,我们为了节省代码可以提取出一个基本类,然后由其他类继承这个基本类。

但是这个时候就会遇到一个问题,那就当执行数据库迁移命令的时候,会连同这张表一并创建到数据库,当不需要数据库新建出表的时候,就可以使用到抽象表

代码示例:

class BaseModel(models.Model):is_delete=models.BooleanField(default=False)# auto_now_add=True 只要记录创建,不需要手动插入时间,自动把当前时间插入create_time=models.DateTimeField(auto_now_add=True,null=True)# auto_now=True,只要更新,就会把当前时间插入last_update_time=models.DateTimeField(auto_now=True,null=True)class Meta:abstract=True  # 抽象表,不再数据库建立出表class Book(BaseModel):title = models.CharField(max_length=32)price = models.DecimalField(max_digits=8,decimal_places=2)# db_constraint=False  逻辑上的关联,实质上没有外键练习,增删不会受外键影响,但是orm查询不影响bind_publish = models.ForeignKey(to='Publish',on_delete=models.DO_NOTHING,db_constraint=False)bind_author = models.ManyToManyField(to='Author',through='Book2Author',through_fields=('bind_book','bind_author'))class Publish(BaseModel):name = models.CharField(max_length=32)addr = models.CharField(max_length=64)
  • 现在Book表和Publish表都有着共同的几个字段:是否删除、创建时间、更新时间

    于是我们抽出来一个基类叫BaseModel,该类中定义Meta类,添加属性abstract=True,这样就表示为抽象类,执行数据库迁移命令不会自动创建表。

十一、权限

注意:

  • **“权限”需要与“认证”**结合使用,因为只有登陆过的用户才可以进行权限的校验,判断例如是普通用户还是VIP尊贵客户,没有登陆有才会统一归为匿名用户。
  • 在**“认证”章节中,有提到当认证校验通过之后,需要返回两个值,这两个值会被request.user和request.auth所接收,其中重点是user**,在权限的校验中,需要获取到当前登陆的用户。(认证组件校验成功之后,扭头就处理权限认证)

11.1 权限组件的使用

基本上与认证大差不差

权限使用步骤:

  • 写一个类,继承BasePermission,重写has_permission,如果权限通过,就返回True,不通过就返回False

    权限就不需要再抛出异常了。

一、书写认证代码

from rest_framework.permissions import BasePermissionclass UserPermission(BasePermission):def  has_permission(self, request, view):# 由于认证已经过了,并且返回了用户对象,request.user拿到当前登录用户对象user = request.user  # 在表设计中,User表的user_type字段用于记录用户类型,1为超级用户。# 不是超级用户的不可以调用if user.user_type==1:return Trueelse:return False

二、全局/局部使用

# 局部使用
class TestView(APIView):permission_classes = [app_auth.UserPermission]# 全局使用
REST_FRAMEWORK={  "DEFAULT_AUTHENTICATION_CLASSES":["导入自定义认证类的模块路径",],'DEFAULT_PERMISSION_CLASSES': ['导入自定义权限类的模块路径',],
}
# 局部禁用
class TestView(APIView):permission_classes = []
  • 与认证一样,当配置了全局校验时,关键视图需要局部禁用。

十二、频率

根据用户访问IP来限制访问次数:

  • 一、写一个类,继承SimpleRateThrottle,只需要重写get_cache_key方法
  • 二、全局/局部使用

代码示例:

  • 应用.tools.throttle.py

    from rest_framework.throttling import SimpleRateThrottleclass MyThrottle(SimpleRateThrottle):scope='frequency'def get_cache_key(self, request, view):return request.META.get('REMOTE_ADDR')  # 返回访问的IP地址
    
  • 全局使用:

    # settings.py文件
    REST_FRAMEWORK={"DEFAULT_THROTTLE_CLASSES": ["app01.tools.throttle.MyThrottle", ],'DEFAULT_THROTTLE_RATES': {'frequency': '3/m'    #这里需要与自定义频率类定义的scope值一致,frequency单词意为:频率},
    }
    

    局部使用:

    # settings.py文件
    REST_FRAMEWORK={#"DEFAULT_THROTTLE_CLASSES": ["app01.tools.throttle.MyThrottle", ],'DEFAULT_THROTTLE_RATES': {'frequency': '3/m'},
    }# views.py文件
    from app01.tools.throttle import MyThrottle
    class Books(APIView):# 局部使用频率限制throttle_classes = [MyThrottle,]def get(self,request):...
    

至此,所有用户再来访问的时候都是一分钟三次,因为是基于IP进行限制的。

附:

  • “3/m”表示一分钟可以访问三次,m代表分钟,在源码中这里只取首字母,改为“3/mm”都不会报错。
  • 同理,既然“/m”表示每分钟,那么每小时就是“/h”

注:后续需要补充

2023-01-29

  • 在教学中,是利用内置限制类完成的,所以用户的认证也要采用内置的,认证所需的用户表是auth模块的user表。

    如果我们使用自己写的user表和usertoken表来进行认证,然后认证通过后再区别未登录登陆过的用户,然后分别设置不同的访问频率,这种需求就需要使用自定义限制类,但是问题就在于,自定义限制类目前没学过,没学过如何针对未登录用户做限制,如何对登陆用户做限制,如何对登陆的VIP用户或者超级用户做限制,所以本章节后续需要再重新补充。

2023-02-08

  • 内置类相关笔记已清空。

    自定义频率限制缺少针对用户的判断,如该用户是不是匿名用户,是不是普通用户,是不是VIP用户,缺乏这方面的频率。

十三、过滤

可实现效果:

  • 在URL中跟上参数,可以根据参数查询出数据

一、安装第三方模块

  • pip3 install django-filter
    

二、配置文件与路由的配置

  • # settings.py
    INSTALLED_APPS = [...略...'django_filters',
    ]# urls.py
    urlpatterns = [path('authors/',views.AuthorViews.as_view()),
    ]
    

三、局部配置

2023-01-30 全局配置没生效,所以这里只写局部了,十四章节中的排序也同样如此。

  • 局部配置:

    from rest_framework.generics import ListAPIView
    from django_filters.rest_framework import DjangoFilterBackendclass AuthorViews(ListAPIView):# 由于ListAPIView继承了GenericAPIView,所以queryset和serializer_class这两个属性都要定义。queryset = Author.objects.all()#自定义的序列化类serializer_class = ser.AuthorModelSerializer  # 局部禁用认证authentication_classes = []# 局部禁用权限permission_classes = []# 局部配置使用内置的过滤类filter_backends = [DjangoFilterBackend]filterset_fields = ('name',)  # 可根据name字段进行查找,可以配置多个可查询字段,但必须是元祖格式。
    

注意事项

  • 视图类继承ListAPIView,其他的不好使。

  • 由于过滤我们没有使用自定义,而是直接使用内置的,所以在视图类中进行局部配置的时候,需要导入内置相关类DjangoFilterBackend

  • 最后需要指定,通过那个字段,来进行过滤,如:filterset_fields = (‘name’,) 根据name字段的值来进行过滤。

  • filterset_fields 的值,必须是个元祖,所以上述代码中,就写成了**(‘name’,)**

    附:python中元组数据类型的多种赋值方式

    a = 1,
    b = (1,)
    c = 1,2
    d = (1,2)
    print(type(a))  # <class 'tuple'>
    print(type(b))  # <class 'tuple'>
    print(type(c))  # <class 'tuple'>
    print(type(d))  # <class 'tuple'>
    

十四、排序

无全局配置

与过滤差不多,代码示例:

局部配置:

from rest_framework.generics import ListAPIView
from rest_framework.filters import OrderingFilter #导入内置排序类
from app01.models import Book
from app01.ser import BookSerializer
class Book2View(ListAPIView):queryset = Book.objects.all()serializer_class = BookSerializer# 排序(与过滤共用一个filter_backends,所以可以再把过滤写上)filter_backends = [OrderingFilter]ordering_fields = ('id', 'price')

如何使用?

  • 例如:根据id值进行排序

    http://127.0.0.1:8000/test/?ordering=-id   #反向(id值从大到小)
    http://127.0.0.1:8000/test/?ordering=id    #正向(id值从小到大)
    

十五、自定义异常处理

在前面的认证、权限、频率等章节当中,当我们的访问出问题,比如没有携带token、没权限、频率上限了,这个时候会直接抛出异常,如:

可以发现并没有携带请求响应状态码,也没有msg信息,这显然不符合规范,那么我们就需要进行完善。

drf内置的有异常处理方法,但是只对请求是否是404、是否具有认证权限频率的异常,最后return none表示,其他的异常就不处理了,交给django自己去做处理,但交给django去处理的话,返回的就肯定不是JSON格式的数据(而是一个报错页面),所以我们需要写一个自定义异常类,对于内置的方法进行完善。

书写自定义异常处理方法:

from rest_framework.views import exception_handler  # 内置的异常处理方法
from rest_framework.response import Response
from rest_framework import status
def my_exception_handler(exc, context):# exc为异常对象,记录了错误信息及状态码。# context为字典,里面详细记录了哪个视图函数报了怎么错,后续可做日志记录等response = exception_handler(exc, context)  # 先执行内置的异常处理'''内置的异常处理方法会有两种返回值,一种是none,一种是response为none表示这个异常,内置的处理不了,直接交给django处理为response表示,这个异常内置处理的(但是并不符合规范,所以我们后面针对这种可能,也要做进一步处理。)'''# 如果内置处理不了,那么就由下面的代码做处理if not response:# 可以做更精细粒度的异常处理,如视图中数字运算出现分母为0的情况,那么就可以书写下列两行代码,对这种类型的错误进行异常处理。if isinstance(exc, ZeroDivisionError):return Response(data={'status': 701, 'msg': "除以0的错误" + str(exc)}, status=status.HTTP_400_BAD_REQUEST)# 如果内置的处理不了,那么就返回符合规范的JSON数据。return Response(data={'status':1000,'msg':str(exc)},status=status.HTTP_400_BAD_REQUEST)else:# 如果内置的可以处理,说明就是认证、权限、频率这一块被查出来异常了#由于默认只返回 {detail:xxxx},并没有响应码之类的,所以我们这边做了进一步的处理。return Response(data={'status':888,'msg':response.data.get('detail')},status=status.HTTP_400_BAD_REQUEST)

全局配置

'EXCEPTION_HANDLER': '应用名.自定义的模块路径',
  • 没有局部配置,配置之后会先查找django项目.settings,然后再查询rest_framework.settings,我们在全局配置定于说是覆盖掉原本内置的,然后类似于装饰器一样,对原有的异常处理做了进一步处理。

十六、封装Response

5.8章节中的扩展,进一步封装response。

  • 由于drf原生response没有那么多参数,但是我们在返回给前端的时候又恰恰需要这些数据,所以就需要进一步封装response,并且扩展性非常高,后端视图可以返回任意数据,比如token、header等等。
  • 所以在后面就可以直接使用我们自己封装的response。
from rest_framework.response import Responseclass APIResponse(Response):def __init__(self,code=100,msg='成功',data=None,status=None,headers=None,**kwargs):dic = {'code': code, 'msg': msg}if  data:dic = {'code': code, 'msg': msg,'data':data}dic.update(kwargs)super().__init__(data=dic, status=status,headers=headers)# 使用
return APIResponse(data={"name":'liuyu'},token='111-222-333',others='abcdefg')
return APIResponse(data={"name":'liuyu'})
return APIResponse(code='101',msg='错误',data={"name":'liuyu'},token='111-222-333',others='abcdefg',header={})

十七、练习:图书表的批量增删改查

路由:

urlpatterns = [path('book/', views.BookView.as_view()),re_path('book/(?P<pk>\d+)', views.BookDetailView.as_view()),
]

模型:

class BaseModel(models.Model):is_delete=models.BooleanField(default=False)create_time=models.DateTimeField(auto_now_add=True,null=True)last_update_time=models.DateTimeField(auto_now=True,null=True)class Meta:abstract=True  # 抽象表,不再数据库建立出表class Book(BaseModel):title = models.CharField(max_length=32)price = models.DecimalField(max_digits=8,decimal_places=2)bind_publish = models.ForeignKey(to='Publish',on_delete=models.DO_NOTHING,db_constraint=False)bind_author = models.ManyToManyField(to='Author',through='Book2Author',through_fields=('bind_book','bind_author'))# 序列化器中fields属性使用@propertydef publish_name(self):return self.bind_publish.name@propertydef author_list(self):author_list=self.bind_author.all()return [ {'name':author.name,'sex':author.get_sex_display()}  for author in author_list]class Publish(BaseModel):name = models.CharField(max_length=32)addr = models.CharField(max_length=64)class Author(BaseModel):name = models.CharField(max_length=16)sex = models.IntegerField(choices=((1, '男'), (2, '女')),null=True)author_detail = models.OneToOneField(to='AuthorDetail',on_delete=models.CASCADE)class AuthorDetail(BaseModel):msg = models.TextField()

序列化器:

class BookSer(ModelSerializer):def __init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.fields['bind_publish'].write_only = Trueself.fields['bind_author'].write_only = Trueclass Meta:model = models.Book# 仅序列化这些字段,列表中均为模型层中类的属性,那么既然是属性,方法也可以伪装成属性,# 'bind_publish','bind_author'就是类中的伪属性方法。fields = ['title','price','publish_name','author_list','bind_publish','bind_author']extra_kwargs={'id':{'read_only':True},'publish_name':{'read_only':True},'author_list':{'read_only':True},}

视图:

from rest_framework.mixins import ListModelMixin,CreateModelMixin,UpdateModelMixin, DestroyModelMixin,RetrieveModelMixin
from rest_framework.generics import GenericAPIView,ListAPIView
from app01.tools import ser # 自定义序列化类,ser.BookSer# 负责全查、单增多增、多更新
class BookTestView(ListAPIView, ListModelMixin, CreateModelMixin):queryset = Book.objectsserializer_class = ser.BookSer# 全查def get(self, request):return self.list(request)# 单增和多增def post(self, request):# 如果提交的数据格式为字典,这里默认为是单条数据,# 如:{"title": "测试","price": "778","bind_publish": "4","bind_author": "7",}if isinstance(request.data,dict):return self.create(request)  # 直接使用ListModelMixin扩展类中的create方法即可创建。# 如果是列表格式,那默认是批量新增数据# 如:[{"title": "测试1","price": "778"},{"title": "测试2","price": "885"}]elif isinstance(request.data,list):# 序列化多条数据book_ser = ser.BookSer(data=request.data,many=True)# 提交过来的数据经过序列化组件校验,判断是否合法。if book_ser.is_valid(raise_exception=True):# 如果指定raise_exception=True,其实可以不用书写if判断,因为直接就抛出异常了,然后经过内置异常+自定义异常完成response处理。book_ser.save()return Response(book_ser.data)else:return Response({'status': 101, 'msg': '校验失败'})# 批量更新def put(self,request):# 当提交的数据为列表格式时,视为批量更新# 如:  http://127.0.0.1:8000/book  请求方式put# 发送数据: [{'id':1,'name':'流浪地球2','price':47.8},{'id':2,'name':'第九区','publish':'文学出版社'}]# 意为: 将id为1的图书,名称改为流浪地球2,价格为47.8。  id为2的图书,名称改为第九区,出版社改为文学出版社。if isinstance(request.data,list):# 被修改图书的列表book_list = []# 需要修改的内容(不包含ID因为ID不可被修改,所以pop删除掉)modify_book_list = []for book_obj in request.data:# 会拿到被删除的值,该值为id值,不可用于修改,所在需要对将要修改的数据做处理。pk = book_obj.pop('id')    # 此时的book_obj {'name':'流浪地球2','price':47.8}modify_book_list.append(book_obj)# 拿到需要进行修改的图书book = Book.objects.filter(pk=pk).first()book_list.append(book)# enumerate枚举for index,book_obj in enumerate(book_list):# index为图书列表中当前对象的索引# book_obj为对象book_ser = ser.BookSer(instance=book_obj,data=modify_book_list[index],partial=True)book_ser.is_valid(raise_exception=True)  # 使用内置的异常处理,所以就不If判断了。book_ser.save()return Response({'status':200,'msg':'更新成功'})else:return Response({'status':400,'msg':'更新失败'})# 单个删除,略# 负责单查,单更新
class BookDetailTestView(GenericAPIView, RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin):queryset = Book.objectsserializer_class = ser.BookSer# 单查def get(self, request, pk):return self.retrieve(request, pk)# 单个更新def put(self, request, pk):return self.update(request,pk)def delete(self, request, pk):return self.destroy(request, pk)# 单个删除,略。

十八、分页器

内置的分页器有三种:

  • PageNumberPagination
  • LimitOffsetPagination
  • CursorPagination

2.1 PageNumberPagination

基本分页

使用方法:

  • 视图层导入PageNumberPagination模块
  • 配置分页

注意事项:

  • 视图类需要继承ListAPIView,由于ListAPIView继承自GenericAPIView,所以之前使用GenericAPIView书写的接口可以直接替换即成为ListAPIView即可。

  • 视图类中的queryset属性必须是对象,不可以是QuerySet对象

    queryset = Book.objects.all()
    

代码示例:

pagination_class = PageNumberPagination

from rest_framework.pagination import PageNumberPagination# 代码沿用十七章节,路由、序列化类均不在重复书写。
class BookView(ListAPIView, ListModelMixin, CreateModelMixin):queryset = Book.objectsserializer_class = ser.BookSer# 配置分页pagination_class = PageNumberPaginationdef get(self, request):return self.list(request)

分页的条数等其他配置,有两种方式操作:

  • settings.py配置

    # settings.py文件
    REST_FRAMEWORK={# 分页'DEFAULT_PAGINATION_CLASS':  'rest_framework.pagination.PageNumberPagination','PAGE_SIZE': 3  # 每页数目
    }
    
  • 也可以在子类中定义属性

    # views.py文件
    class MyPageNumberPagination(PageNumberPagination):page_size=3  # 一页展示三条class BookTestView(ListAPIView):queryset = Book.objects.all()  #不叫all会报错serializer_class = ser.BookSer# 配置分页pagination_class = MyPageNumberPagination   # 指定成子类
    

效果展示:

2.1.1 参数详解

除了控制每页的返回条数以外,还可以配置其他的:

  • page_size = 5 每页条数,每页4条

  • page_query_param=‘p’ 查询第几页的key,默认是page,示例中修改为 p

    默认按照页数查询时URL:http://127.0.0.1:8000/book/?page=2 表示查询第二页

    现在改成了: http://127.0.0.1:8000/book/?p=2

  • page_size_query_param=‘size’ 配置后可以在URL中选择每页显示的条数,示例中=size

    例如:http://127.0.0.1:8000/book/&size=6 http://127.0.0.1:8000/book/?page=1&size=6

    配置之后可以突破page_size=3的限制,展示更多条数据,但是同时又被max_page_size每页最大显示条数所限制

  • max_page_size = 5 每页最大显示条数,示例为最大显示5条。

2.2 LimitOffsetPagination

偏移分页

代码示例:

from rest_framework.pagination import LimitOffsetPagination
class MyLimitOffsetPagination(LimitOffsetPagination):default_limit = 5  #默认一页是多少条#offset_query_param = 'offset' # 从哪里开始取的key#limit_query_param = 'limit' # 取多少条的keymax_limit = 5  # 最多可以取多少条class BookTestView(ListAPIView, ListModelMixin, CreateModelMixin):queryset = Book.objects.all()serializer_class = ser.BookSer# 配置分页pagination_class = MyLimitOffsetPagination

2.2.1 参数详解

常用参数:

  • default_limit = 5 默认每页条数,5条

  • offset_query_param = ‘offset’ 从哪里开始查询的key

    offset_query_param = ‘offset’ 与 limit_query_param = ‘limit’ 连用,一般不修改,直接省略默认就是offset和limit

    表示从哪里开始取,这一页取几位。

  • limit_query_param = ‘limit’ 取几位的key

    两个参数不写默认就是offset和limit,使用示例:http://127.0.0.1:8000/book/?limit=5&offset=0

    offset=0表示从主键值0往后取,取limit=5,也就是5位,但是这个与切片类似,同样是“顾头不顾腚”,因此想要取id为1-5时,offset需要等于-1等于0

  • max_limit = 5 一页最大只能取多少,示例为5条。

2.3 CursorPagination

游标分页

该分页器的特性:

  • 速度最快,但是只支持上一页和下一页

代码示例:

from rest_framework.pagination import CursorPagination
class MyCursorPagination(CursorPagination):#cursor_query_param = 'cursor' # 每一页查询的keypage_size = 3  # 每页显示的条数ordering = '-id' # 排序字段class BookTestView(ListAPIView, ListModelMixin, CreateModelMixin):queryset = Book.objects.all()serializer_class = ser.BookSer# 配置分页pagination_class = MyCursorPagination

2.3.1 参数详解

常用参数:

  • cursor_query_param = ‘cursor’ 每一页查询的key,默认就好不需要改

    由于游标分页只支持上下页,因为每一页都打上了标记,所以在面对大量数据的时候可以做到响应速度特别特别快。

    返回数据的时候会带上 “next"与"previous”,如:

    ​ “next”: “http://127.0.0.1:8000/book/?cursor=cD04”,

     "previous": "http://127.0.0.1:8000/book/?cursor=cj0xJnA9MTA%3D",
    

    所以这个参数的作用就是这个,默认就是cursor,不用修改就好。

  • page_size = 3 每页显示的条数

  • ordering = ‘-id’ 根据那个字段进行排序,“-”为反向排序,不加表示正向。

2.4 报错:‘Manager’ object is not subscriptable

出现这个报错是因为视图类中的queryset参数,赋值的时候偷懒,自己不写**.all()**,想让GenericAPIView补充

但是这里不自己加上**.all()**就会报这个错。

2.5 使用APIView或GenericAPIView

在上文中说道,使用分页器需要继承ListAPIView,但其实APIView和GenericAPIView也是可以使用的,只不过封装程度没有那么多,需要手动书写一些代码,不过换来的是可控性很高。

代码示例:

class TestTest(APIView):def get(self,request):book_list = Book.objects.filter(is_delete=False)# 实例化分页器对象page_obj = MyPageNumberPagination()# 拿到图书分页queryset,也可以再获取上一页和下一页(因为游标分类需要)book_page_queryset = page_obj.paginate_queryset(book_list,request)next_url = page_obj.get_next_link()previous_url = page_obj.get_previous_link()# 将分页器处理好的querset对象进行序列化处理book_ser = ser.BookSer(book_page_queryset,many=True)# 完善response并返回return Response({'next':next_url,'previous':previous_url,'results':book_ser.data})
  • GenericAPIView与APIView没区别,都一样。

十九、coreapi

作用:

  • 自动生成API文档

一、安装:

  • pip3 install coreapi
    

二、路由与settings配置:

  • urls

    from rest_framework.documentation import include_docs_urlsurlpatterns = [  path('docs/', include_docs_urls(title='API接口文档')),
    ]
    
  • settings

    REST_FRAMEWORK={# 自动生成API文档'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',}
    

三、视图类添加注释

  • views

    class BookTestView(ListAPIView, ListModelMixin, CreateModelMixin):'''get:返回所有图书信息post:新增图书put:更新图书信息'''def get(self, request):passdef post(self, request):passdef put(self, request):pass
    

    如果该接口是单一方法,也就是说内部只有例如get方法,那么直接写注释。

    如果该接口包含了多个方法,那么就需要使用“ 方法名 :注释信息”。

查看效果:

  • 上图中create接口有几个必传参数,如"title"、“price”、"bind_publish"后面的描述信息原本是没有的,需要在models中给对应字段添加help_text属性。

    # 示例:
    class Book(models.Model):title = models.CharField(max_length=32, help_text='图书标题')price = models.DecimalField(max_digits=8,decimal_places=2, help_text='图书价格')
    

另外这个文档也是可以直接用来测试接口的:

  • page 和 page size,这两个参数为分页器的参数,非必填,但是coreapi也会展示出来,供我们测试接口。

二十、jwt

先安装模块

pip3 install djangorestframework-jwt

什么是jwt:

  • Json Web Token,是基于Json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,他的两大使用场景是:认证和数据交换

jwt生成原理

  • jwt分三段式:头.体.签名

    是可逆加密的,让服务器可以反解出user对象。

    签名是不可逆加密,保证整个token的安全性。

  • 头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,

    不可逆加密一般采用hash(md5)算法

  • 中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息

    {"company": "公司信息",...
    }
    

    中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间

    {"user_id": 1,...
    }
    

    签名中的内容是安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码,加在一起再进行md5加密。

    {"head": "头的加密字符串","payload": "体的加密字符串","secret_key": "安全码"
    }
    

校验

  • 将token按 “.” 拆分为三段字符串,第一段为加密字符串,一般不需要做任何处理。
  • 第二段为加密字符串,要反解出用户主键,然后通过主键从User表中得到登录用户,过期时间和设备信息等安全信息,确保token没过期,且是同一设备来的。
  • 用 md5加密(第一段 + 第二段 + 服务器安全码) = 第三段的签名,随后与请求的第三段签名字符串进行碰撞校验,通过后才能代表这个签名是服务端给的,第二段校验得到的user对象才是合法的登录用户。

drf项目的jwt认证开发流程

  • 用账号密码访问登录接口,登录接口逻辑中调用签发token 算法,得到token,返回给客户端,客户端自己存到cookies中

  • 校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户

    注:登录接口需要做 认证 + 权限 两个局部禁用

20.1 jwt基本认证使用

前言:

  • 由于django还内置了基于角色的控制,所以一般还是使用auth模块的user表,当然了使用自定义的usertoken表也是没有问题的,但是这里使用的auth模块的user表,这个时候十章节—>十二章节的组件可以使用内置的了,但是本文介绍的都是自定义校验,要使用内置的需要自行查询文档。

扩展auth user表

auth模块的user表没有手机号字段,没有头像字段等,所以这里我们选择进行扩展

模型层models.py

from django.contrib.auth.models import AbstractUser  # 与auth.user表一样,都继承AbstractUser# 也叫user没关系,因为会带上应用名前缀,如api_user,另外一个是auth_user
class User(AbstractUser):phone = models.CharField(max_length=11)photo = models.ImageField(upload_to='icon')  # ImageField字段依赖于pillow模块,pip3 install pillow
  • 扩展auth.user表还需要在settings.py文件中进行配置,同时上传的头像这里选择保存在项目指定目录,后续再介绍上传到云cos对象存储或者redis等。

    # 扩展user表
    AUTH_USER_MODEL = 'api.User'MEDIA_URL='/media/'
    MEDIA_ROOT=os.path.join(BASE_DIR,'media')
    

扩展之后,创建超级用户

  • python manage.py createsuperuser
    

20.1.1 获取token

三种方法:

  • ObtainJSONWebToken.as_view()
  • obtain_jwt_token
  • 自定义

前两种都是使用jwt内置的ObtainJSONWebToken类,这个类又是视图类,里面写好了token内,头、荷载、签名的处理,并且返回值就是序列化之后的。所以我们可以直接在路由中使用该内置视图类ObtainJSONWebToken.as_view()

obtain_jwt_token是ObtainJSONWebToken视图类中,帮我们直接调用了as_view(),所以第二种等同于第一种

如果想要在“打包”三段token的时候自己可控,那么就可以自己书写逻辑即可。

代码示例(前两种):

#from rest_framework_jwt.views import ObtainJSONWebToken   # 二选一
from rest_framework_jwt.views import obtain_jwt_tokenurlpatterns = [# path('login/', ObtainJSONWebToken.as_view()),  # 二选一path('login/', obtain_jwt_token),
]
  • ObtainJSONWebToken视图类中没有定义请求方法,但是它所继承的基类JSONWebTokenAPIView是有一个post方法的。

    所以我们在路由配置好之后,就可以提交POST请求来获取token了,由于本章节使用的是扩展的auth user表,所以在提交POST请求的时候携带的key值为username和password

接口请求示例:

第三种(自定义):

  • 上面利用内置类/方法写的login接口属于“自动签发”。

    手动签发需要将user对象拿到,然后转成payload,再转成token,最后序列化返回

详细代码略,核心操作就下面这些。

from rest_framework_jwt.utils import jwt_payload_handler
from rest_framework_jwt.utils import jwt_encode_handlerpayload = jwt_payload_handler(user)  # 将获取到的用户对象做成荷载
token = jwt_encode_handler(payload)  # 合成token

20.1.2 校验token

现在客户端拿到了token值,那么携带着token去进行访问的时候,需要在请求的头部,指定Authorization属性,值为“jwt token值”,由于内置的必须要在前面写上 “jwt和 空格” ,所以这里我们利用第十章节的自定义认证来进行处理,在这个过程中,如何对第二段校验、反取user、第三段的校验等,这里使用jwt给我们封装好的方法。

请求示例:

  • Authorization的值就是token,但是需要加上前缀,以及空格。

代码示例:

  • 自定义校验类:

    from rest_framework import exceptions# BaseJSONWebTokenAuthentication继承于drf的BaseAuthentication
    # 这里使用子类BaseJSONWebTokenAuthentication是因为含有authenticate_credentials方法,可从荷载中获取用户对象,而drf原生的认证类并没有。
    class MyToken(BaseJSONWebTokenAuthentication):def authenticate(self, request):# 根据规定从请求头中获取tokenjwt_value = str(request.META.get('HTTP_AUTHORIZATION'))# 认证try:payload = jwt_decode_handler(jwt_value)except Exception:raise exceptions.AuthenticationFailed("认证失败")# 获取用户对象    user = self.authenticate_credentials(payload)return user,None
    
    • jwt_value:表示从请求头中,获取到携带的token

    • payload:将token的三段拆开,解析出第二段的payload,同时检验有没有超时,有没有篡改。

    • user:payload荷载为用户相关的信息,数据格式为字典,调用authenticate_credentials方法转成user用户数据对象。

      这个user对象就是当前登陆的用户,根据自定义认证类的原则,需要返回两个人值,一个是当前用户对象,一个随便,主要是用户对象,稍后可能还需要对权限进行校验。

  • 视图类

    # 视图
    from api.utils.auth import MyToken
    class BookView(APIView):# 使用自定义校验认证,局部配置authentication_classes = [MyToken,]permission_classes = [IsAuthenticated]def get(self,request):print(request.user.email)return Response('ok')
    

    全局配置:和drf的认证一样

    REST_FRAMEWORK={"DEFAULT_AUTHENTICATION_CLASSES":["api.utils.auth.MyToken",]
    }
    

接口访问示例:

20.1.2.1 内置校验

在20.1.2章节的开头,我们使用的是自定义校验,原因是内置的在传值时较为麻烦,但是内置的是有其他应用场景的。

附:

内置的认证,之所以需要在访问的时候在头部添加参数,并且还要是 “jwt+空格+token” 的形式,是因为jwt默认把当前来访问的用户是匿名用户,不带token也可以进行访问。但如果又在全局或者局部配置了内置的权限 permission_classes = [IsAuthenticated] #判断当前用户是否登陆 ,那么就代表只有登陆成功的用户才可以访问,匿名就不可以了。
我们在上一章节中,是觉得内置不好用,所以才自定义的,现在再回过头就发现,使用自定义auth认证那就必须携带token了。

代码示例:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticatedfrom api.auth import MyToken
class BookView(APIView):# 使用内置的校验tokenauthentication_classes = [JSONWebTokenAuthentication]permission_classes = [IsAuthenticated]def get(self,request):print(request.user.email)return Response('ok')'''
auth模块中的is_authenticated()方法,就是校验当前用户是否经过校验
同理,内置的IsAuthenticated权限类也是,也是校验当前用户是否经过校验
'''

接口访问示例:

  • 因为使用的是内置的,所以需要 “jwt+空格” ,大小写无所谓。上图中是没有加空格,所以会被认为是匿名用户。

    但是配置的权限认证是IsAuthenticated,就是校验是否是登陆用户的,所以此时该接口只能登录用户才可以访问。

20.2 自定义返回数据格式

在20.1章节中,我们实现的简单使用,并没有状态码之类的信息,所以本章节就是对20.1的进一步补充,对签发token和检验token的业务代码进行完善。

20.2.1 获取token

查看源码:

  • 由于使用的内置的,所以jwt的settings中可以找到配置的认证类

    from rest_framework_jwt import  settings'JWT_RESPONSE_PAYLOAD_HANDLER':'rest_framework_jwt.utils.jwt_response_payload_handler',# 查看内置的处理响应方法
    from rest_framework_jwt.utils import jwt_response_payload_handler
    

    jwt_response_payload_handler就是最终返回token的方法,如下:

    def jwt_response_payload_handler(token, user=None, request=None):return {'token': token}
    

    可以看到,就只返回了token,这和我们在使用postman或者apipost接口测试工具的时候,返回的结果是一样的。

自定义获取token的返回值:

  • 有了上面的分析,我们在自定义的时候直接重写源码中这个方法就好,然后在settings中配置一下,响应走我们重写的jwt_response_payload_handler方法。

    # 示例新建路径:应用api-->utils文件夹-->auth.py
    # views.py
    # 自定义获取token的返回值
    def jwt_response_payload_handler(token, user=None, request=None):return {'status':status.HTTP_200_OK,'user':user.username,'token': token}# settings.py
    JWT_AUTH = {'JWT_RESPONSE_PAYLOAD_HANDLER':'api.utils.auth.jwt_response_payload_handler'
    }
    

    接口示例:

20.2.2 校验token

自定义校验token的返回数据格式:

  • 在20.1.2章节中,我们知道了校验token分为两种,一种是使用内置的JSONWebTokenAuthentication,一种是自定义。
  • 既然本章节是自定义返回值,那么肯定使用自定义认证,不再内置里面再重写各种方法了。

自定义认证基类:

  • BaseJSONWebTokenAuthentication DRF-JWT章节使用的认证基类

  • BaseAuthentication DRF章节使用的认证基类

    二者没什么大的区别,前者为后者的子类,在原有的基础上封装了用于处理token三段的方法。

    本章节将对继承BaseJSONWebTokenAuthentication和BaseAuthentication,分别介绍如何完成自定义返回值。

代码推导:

  • 自定义认证token,可以更加精细化的捕获异常,最后根据自定义认证类规则,再返回两个值。

    from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication,BaseAuthentication#BaseJSONWebTokenAuthentication源码中导入了BaseAuthentication,所以都在jwt中。
    from rest_framework_jwt.utils import jwt_decode_handler
    from rest_framework.exceptions import AuthenticationFailed  #可以抛出身份校验失败的异常
    import jwt  # 内有jwt校验的几种错误类型class MyToken(BaseAuthentication):def authenticate(self, request):jwt_value=str(request.META.get('HTTP_AUTHORIZATION'))if jwt_value:try:# 如果携带token那就尝试着校验,并拿到payloadpayload = jwt_decode_handler(jwt_value)except jwt.ExpiredSignature: # 过期签名错误raise AuthenticationFailed('签名过期')except jwt.InvalidTokenError:  # 无效令牌错误raise AuthenticationFailed('无效用户')except Exception as e:  # 捕获其他的全部异常raise AuthenticationFailed(str(e))#print(payload)  #{'user_id': 1, 'username': 'liuyu', 'exp': 1676233125, 'email': '17506166294@163.com'}user = User(id=payload.get('user_id'),username=payload.get('username'))'''这样子可以节省去数据库查询的时间。'''return user,jwt_valueraise  AuthenticationFailed('没有多携带认证信息')
    

    补充:利用模型类创建用户数据对象

    from api.models import Userprint(type(User.objects.filter(pk=1).first()))   #<class 'api.models.User'>
    print(type(User(id=1,username='liuyu',password='11223355')))  # <class 'api.models.User'>
    

继承BaseJSONWebTokenAuthentication

  • 上面的代码都是继承BaseAuthentication认证基类完成的,JWT子类中把payload —转–> userobj 这一步封装成了一个方法:authenticate_credentials

    class MyToken(BaseJSONWebTokenAuthentication):def authenticate(self, request):jwt_value=str(request.META.get('HTTP_AUTHORIZATION'))# 认证if jwt_value:try:payload = jwt_decode_handler(jwt_value)except jwt.ExpiredSignature:raise AuthenticationFailed('签名过期')except jwt.InvalidTokenError:raise AuthenticationFailed('无效用户')except Exception as e:raise AuthenticationFailed(str(e))# 但核心也是User(id=payload.get('user_id'),username=payload.get('username'))user = self.authenticate_credentials(payload)return user,jwt_valueraise  AuthenticationFailed('没有多携带认证信息')
    

20.2.3 总结

自定义签发和检验token返回数据格式、自定义异常处理、自定义Response对象,这三者的区别:

  • 本章节的自定义返回数据: 用于返回符合规范的数据,以及做整一些常规异常捕获,针对签名过期、签名非法等情况来返回合适的数据。
  • 自定义异常处理: 直接抛出异常,django能处理就处理,处理不了就不管了,来了个大大的HTML页面报错。但是django处理的不行,所以我们需要分情况都做统一的异常处理。
  • 自定义Response: 接口返回status、msg等信息时会遇到代码冗余,所以我们可以封装Response,节省重复代码。

签发token返回数据的一些问题:

  • 现象:签发token,如果输入的username和password有错误,那么会直接抛出异常,如果这个时候没有进行自定义异常处理,那么返回的结果没有响应状态码,这就不符合规范了。
    示例:签发token的异常处理

    • 签发token作为三大认证的一环,drf呢又对三大认证进行内置异常处理,所以当输入的用户名或密码错误时,drf会处理好错误信息,封装到response.data中,如果没有配置自定义异常捕获,那么django drf处理好之后就直接返回了,所以我们只需要在自定义异常处理这边多写一点而已,完善下返回结果。

      完善之后接口访问示例:

校验token:

  • 由于在自定义校验类中就进行了异常捕获处理,所以校验类中定义好返回的数据就可以了。 签发token倒是需要完善自定义异常处理

20.3 自动签发应用:多方式登陆

在一些登陆场景中,我们可以发现有一些是输入身份证号+密码、手机号+密码、学号+密码等等,多种方式都可以登陆效果

步骤:

  • 1、由于是自动签发,那么就需要自己去书写login登陆接口,不再使用obtain_jwt_token,那么在书写视图类的时候,由于登陆是post提交,但是post通常又用于create新增数据,所以这里可以利用ViewSet,将视图方法更改为更加合理的名称,而不知局限于get post 。

    修改之后,访问docs页面:

  • 2、用户提交过来的数据,会先被视图类接收,这个时候可以交给序列化类,通过钩子函数来完成校验,分析提交的username字段,到底是用户名、还是手机号、还是邮箱号,与对应的密码是否正确等。

    这个过程中,视图类与序列化类之间可以传输数据,比如序列化类校验完毕之后,将当前的用户对象返回给视图类; 视图类中把request交给序列化类,然后在序列化类中进行各种逻辑书写,不只局限于视图中书写逻辑。

    序列化对象的context属性,就是用来传输值的。

  • 3、最后需要再重新覆盖username字段,不进行数据库的校验,因为它会以为我们是要新建保存数据,所以会校验,这个时候我们再去登陆,它就会报错,说数据已存在,因为数据是是unique唯一。

代码示例:

  • 路由

    urlpatterns = [path('login/', views.LoginView.as_view({'post':'login'})),
    ]
    
  • 视图类

    from rest_framework.viewsets import ViewSetclass LoginView(ViewSet):  #'''login:登陆接口'''def login(self, request, *args, **kwargs):# 自定义序列化类中,传入数据,context属性赋值,可以实现传递到序列化类中使用的效果。login_ser = ser.LoginModelSerializer(data=request.data,context={'request':request})'''序列化类处理'''# 调用is_validad判断处理结果login_ser.is_valid(raise_exception=True)token=login_ser.context.get('token')# 4 returnreturn Response({'status':100,'msg':'登录成功','token':token,'username':login_ser.context.get('username')})
    
  • 序列化类

    import re
    from rest_framework import serializers
    from rest_framework.exceptions import ValidationError
    from rest_framework_jwt.utils import jwt_encode_handler,jwt_payload_handler
    from api import models  #导入应用下面的模型class LoginModelSerializer(serializers.ModelSerializer):# 重新覆盖username字段,数据中它是unique,post过来它认为我们是要保存数据,这个时候会报数据已存在。username=serializers.CharField()  class Meta:model=models.Userfields=['username','password']def validate(self, attrs):# 业务逻辑username=attrs.get('username') password=attrs.get('password')# 通过判断,username数据不同,查询字段不一样# 正则匹配,如果是手机号if re.match('^1[3-9][0-9]{9}$',username):user=models.User.objects.filter(mobile=username).first()# 如果是邮箱elif re.match('^.+@.+$',username):# 邮箱user=models.User.objects.filter(email=username).first()# 如果是手机号else:user=models.User.objects.filter(username=username).first()# 存在用户if user: # 校验密码,因为是密文,要用check_passwordif user.check_password(password):# 签发tokenpayload = jwt_payload_handler(user)  # 把user传入,得到payloadtoken = jwt_encode_handler(payload)  # 把payload传入,得到token# 可以给序列化对象以属性添加的方式,将token返回给视图层self.context['token']=tokenself.context['username']=user.username# 根据全局钩子函数,这里需要返回attrsreturn attrselse:raise ValidationError('密码错误')else:raise ValidationError('用户不存在')
    

接口请求示例:

20.4 token过期时间

token过期时间:

  • 配置文件中改一下就好
import datetime
JWT_AUTH={'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 过期时间,示例中表示7天才过期。
}

20.5 RBAC

作为补充知识点

RBAC:

  • 基于角色的权限控制
  • django中内置,设计好的表,在执行数据库迁移命令的时候,自动生成,与auth_user表一样。

内有:

  • user表,group表,permission表

    #mermaid-svg-I841Q6qoaAhy872m {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-I841Q6qoaAhy872m .error-icon{fill:#552222;}#mermaid-svg-I841Q6qoaAhy872m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I841Q6qoaAhy872m .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-I841Q6qoaAhy872m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I841Q6qoaAhy872m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I841Q6qoaAhy872m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I841Q6qoaAhy872m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I841Q6qoaAhy872m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I841Q6qoaAhy872m .marker.cross{stroke:#333333;}#mermaid-svg-I841Q6qoaAhy872m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I841Q6qoaAhy872m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-I841Q6qoaAhy872m .cluster-label text{fill:#333;}#mermaid-svg-I841Q6qoaAhy872m .cluster-label span{color:#333;}#mermaid-svg-I841Q6qoaAhy872m .label text,#mermaid-svg-I841Q6qoaAhy872m span{fill:#333;color:#333;}#mermaid-svg-I841Q6qoaAhy872m .node rect,#mermaid-svg-I841Q6qoaAhy872m .node circle,#mermaid-svg-I841Q6qoaAhy872m .node ellipse,#mermaid-svg-I841Q6qoaAhy872m .node polygon,#mermaid-svg-I841Q6qoaAhy872m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-I841Q6qoaAhy872m .node .label{text-align:center;}#mermaid-svg-I841Q6qoaAhy872m .node.clickable{cursor:pointer;}#mermaid-svg-I841Q6qoaAhy872m .arrowheadPath{fill:#333333;}#mermaid-svg-I841Q6qoaAhy872m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-I841Q6qoaAhy872m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-I841Q6qoaAhy872m .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-I841Q6qoaAhy872m .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-I841Q6qoaAhy872m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-I841Q6qoaAhy872m .cluster text{fill:#333;}#mermaid-svg-I841Q6qoaAhy872m .cluster span{color:#333;}#mermaid-svg-I841Q6qoaAhy872m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-I841Q6qoaAhy872m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

    多对多
    多对多
    多对多
    user表
    group表
    permission表

    • 在django的RBAC中,还有第六个表, user --> permission,例如:可以让用户tom,在不绑定部门的情况下,赋与其他权限。

应用场景:

  • 用于后台管理,写CRM、ERP系统时可无需再进行表设计。
  • 有用户访问的对外场景时,就用DRF三大认证来进行权限等校验控制

2023 - 02 - 10

自己目前遇到的问题:

  • 如何对中间表进行操作?

20.6 django缓存

缓存介绍:

  • 在动态网站中,用户所有的请求,服务器都会去数据库中进行相应的增、删、查、改、渲染模板、执行业务逻辑,最后生成用户看到的页面。

    当一个网站的用户访问量很大的时候,每一次的的后台操作,都会消耗很多的服务端资源,所以必须使用缓存来减轻后端服务器的压力。

  • 缓存是将一些常用的数据保存内存或者memcache等非关系型数据库中,在一定的时间内有人来访问这些数据时,则不再去执行数据库及渲染等操作,而是直接从内存或非关系型数据库的缓存中去取得数据,然后返回给用户。

    memcache因为可以缓存图片、视频等资源,所以用的会比较多。

django中的几种缓存模式:

  • 开发调试缓存

  • 内存缓存

  • 文件缓存

  • 数据库缓存

  • Memcache缓存(使用python-memcached模块)

  • Memcache缓存(使用pylibmc模块)

    经常使用的有文件缓存和Mencache缓存

django中的几种缓存的配置:

一、开发调试(此模式为开发调试使用,实际上不执行任何操作)

settings.py文件配置

CACHES = {'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache',  # 缓存后台使用的引擎'TIMEOUT': 300,            # 缓存超时时间(默认300秒,None表示永不过期,0表示立即过期)'OPTIONS':{'MAX_ENTRIES': 300,          # 最大缓存记录的数量(默认300)'CULL_FREQUENCY': 3,          # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)},}
}

二、内存缓存(将缓存内容保存至内存区域中)

settings.py文件配置

CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',  # 指定缓存使用的引擎'LOCATION': 'unique-snowflake',         # 写在内存中的变量的唯一值 'TIMEOUT':300,             # 缓存超时时间(默认为300秒,None表示永不过期)'OPTIONS':{'MAX_ENTRIES': 300,           # 最大缓存记录的数量(默认300)'CULL_FREQUENCY': 3,          # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)}  }
}

三、文件缓存(把缓存数据存储在文件中)

settings.py文件配置

CACHES = {'default': {'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', #指定缓存使用的引擎'LOCATION': '/var/tmp/django_cache',        #指定缓存的路径'TIMEOUT':300,              #缓存超时时间(默认为300秒,None表示永不过期)'OPTIONS':{'MAX_ENTRIES': 300,            # 最大缓存记录的数量(默认300)'CULL_FREQUENCY': 3,           # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)}}
}

四、数据库缓存(把缓存数据存储在数据库中)

settings.py文件配置

CACHES = {'default': {'BACKEND': 'django.core.cache.backends.db.DatabaseCache',  # 指定缓存使用的引擎'LOCATION': 'cache_table',          # 数据库表    'OPTIONS':{'MAX_ENTRIES': 300,           # 最大缓存记录的数量(默认300)'CULL_FREQUENCY': 3,          # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)}  }
}

注意,创建缓存的数据库表使用的语句:

python manage.py createcachetable

五、Memcache缓存(使用python-memcached模块连接memcache)

Memcached是Django原生支持的缓存系统.要使用Memcached,需要下载Memcached的支持库python-memcached或pylibmc.

settings.py文件配置

CACHES = {'default': {'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', # 指定缓存使用的引擎'LOCATION': '192.168.10.100:11211',         # 指定Memcache缓存服务器的IP地址和端口'OPTIONS':{'MAX_ENTRIES': 300,            # 最大缓存记录的数量(默认300)'CULL_FREQUENCY': 3,           # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)}}
}

LOCATION也可以配置成如下:

'LOCATION': 'unix:/tmp/memcached.sock',   # 指定局域网内的主机名加socket套接字为Memcache缓存服务器
'LOCATION': [         # 指定一台或多台其他主机ip地址加端口为Memcache缓存服务器'192.168.10.100:11211','192.168.10.101:11211','192.168.10.102:11211',
]

六、Memcache缓存(使用pylibmc模块连接memcache)

settings.py文件配置CACHES = {'default': {'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',  # 指定缓存使用的引擎'LOCATION':'192.168.10.100:11211',         # 指定本机的11211端口为Memcache缓存服务器'OPTIONS':{'MAX_ENTRIES': 300,            # 最大缓存记录的数量(默认300)'CULL_FREQUENCY': 3,           # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3)},  }}

LOCATION也可以配置成如下:

'LOCATION': '/tmp/memcached.sock',  # 指定某个路径为缓存目录
'LOCATION': [       # 分布式缓存,在多台服务器上运行Memcached进程,程序会把多台服务器当作一个单独的缓存,而不会在每台服务器上复制缓存值'192.168.10.100:11211','192.168.10.101:11211','192.168.10.102:11211',
]

Memcached是基于内存的缓存,数据存储在内存中.所以如果服务器死机的话,数据就会丢失,所以Memcached一般与其他缓存配合使用

Django-16:rest-framework与jwt相关推荐

  1. Akka-CQRS(16)- gRPC用JWT进行权限管理

    前面谈过gRPC的SSL/TLS安全机制,发现设置过程比较复杂:比如证书签名:需要服务端.客户端两头都设置等.想想实际上用JWT会更加便捷,而且更安全和功能强大,因为除JWT的加密签名之外还可以把私密 ...

  2. Django前后端分离1——jwt

    一.前戏 1.base64 方法 作用 参数 返回值 b64encode 将输入的参数转化为base64规则的串 预加密的明文,类型为bytes:例:b'guoxiaonao' base64对应编码的 ...

  3. Django REST framework 认证、权限和频率组件

    认证与权限频率组件 身份验证是将传入请求与一组标识凭据(例如请求来自的用户或其签名的令牌)相关联的机制.然后 权限 和 限制 组件决定是否拒绝这个请求. 简单来说就是: 认证确定了你是谁 权限确定你能 ...

  4. Django JWT认证实现

    配置JWT认证 先通过 pip install djangorestframework 命令下载 Django REST framework 库,再通过 pip install djangorestf ...

  5. Django Python Web应用程序框架简介

    在这个由四部分组成的系列文章的前三篇文章中,比较了不同的Python Web框架,我们介绍了Pyramid , Flask和Tornado Web框架. 我们已经构建了同一个应用程序3次,最终进入了D ...

  6. Django前后端分离实现登录验证码功能

    Django前后端分离实现登录验证码功能 当下最流行最热门的开发方式当属前后端分离开发,分工也更加明确与专注,前端也是越来越难,几天不学习就跟不上节奏,一个月不学习可以好不夸张的说,你已经不适合这个行 ...

  7. Django Python:完整的BUNDLE + Django真实项目2021

    Django和python Bundle:从学习python的所有基础知识到高级python再到UI设计TKINTER,然后是Django 你会学到: 学习编程的基础知识 学习Python编程语言 学 ...

  8. python主要用于后端开发还是前端,Django是用于前端还是后端?

    I often see people claiming their backend is implemented in Django, but isn't Django supposed to be ...

  9. REST framework 基本使用

    安装 pip install djangorestframework djangorestframework 介绍 djangorestframework 主要使用 APIView,其实APIView ...

  10. 小白必看!Django 模板语言基础来啦

    作者 | 单雨 责编 | 胡巍巍 出品 | CSDN(ID:CSDNnews) 前言 为了实现模板封装和复用,提高HTML界面调试便捷性以及前后端解耦等目标,Django定义了自己的网络模板语言. 当 ...

最新文章

  1. CentOS 7.8下安装PyRosetta4
  2. c++找不到标识符_沪C转沪牌流程攻略大全
  3. Smartforms Debug
  4. JAVA版StarDict星际译王简单实现
  5. Revit API创建标高,单位转换
  6. 风机桨叶故障诊断(三) 识别桨叶——初步构建BP神经网络
  7. 麻省理工学院推出数据美国大数据可视化工具
  8. 计算机专业认证协会,2017计算机类专业认证委员会工作总结会在京召开
  9. VS201x的项目属性配置
  10. 没想到你是这样的MethodArgumentNotValidException
  11. 租用游艇问题(pta)
  12. flash在线视频播放器
  13. 愤世嫉俗的程序员,总在某乎发表言论,当起了“键盘侠”
  14. 二叉树的递归遍历(树UVa548 紫书p155)
  15. linux中cmd是什么文件格式,CMD 文件扩展名: 它是什么以及如何打开它?
  16. ssh推送Warning: Permanently added 'gitee.com,120.55.226.24' (ECDSA) to the list of known ho
  17. 劳动保障协管员计算机考试,劳动保障协管员考试试题
  18. 进入IT互联网行业一定要报培训班嘛?
  19. 肿瘤与癌症检测相关产品的生物信息分析
  20. 把金庸小说数据化——关于语言的思一点考

热门文章

  1. 央视影音大屏版apk下载_央视影音大屏版
  2. 新媒体运营教程:运营活跃社群的玩法分析
  3. Python爬虫帮你抢秒杀
  4. hadoop-3.1.3 启动HDFS时报错ERROR: Attempting to operate on hdfs namenode as root的解决方法
  5. 利用pip安装python各种库
  6. Nt*和Zw*开头的函数
  7. 获利40多万,工地技术员自学开发外挂被抓
  8. 网课题库接口高准确率
  9. 7-17 找最贵的书和最便宜的书
  10. 积分签到mysql_简易积分签到功能java代码实现实现Redis缓存数据