Django-16:rest-framework与jwt

16 篇文章 2 订阅

Django-16:rest-framework与jwt

一、web开发模式

1.1 前后端不分离

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

  • image-20230114020308015

1.2 前后端分离

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

  • image-20230114020535641

    分离之后,后端只需要关注于返回的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更直观接口返回的结果。

image-20230114170650106

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

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

2.2 Restful规范

什么是RESTful:

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

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

Restful规范(10条)

一、数据的安全保障

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

二、接口特征表现

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

    如:

    看到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

    image-20230114233024905

四、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是当次请求的request
        self = cls(**initkwargs)  #实例化得到一个对象,index对象
        if hasattr(self, 'get') and not hasattr(self, 'head'):
            self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return 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_allowed
            return 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 APIView

class 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 = cls
        view.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 = args
        self.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 = args
            self.kwargs = kwargs
    		# 重新包装成一个request对象,以后再用的request对象,就是新的request对象了
            request = self.initialize_request(request, *args, **kwargs)
            self.request = request
            self.headers = self.default_response_headers  
    
            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)
    
            # 渲染模块
            '''
            区分出访问的客户端是浏览器还是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>在用法上的区别,及源码分析:

- 没有区别

  ```python
    def __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可以针对浏览器的请求,做出一些好看的页面,如下:

      image-20230116004322319

代码示例:

模型层代码略

一、新建序列化类

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

    image-20230116004501757

    # 序列化类   serializer.py文件
    from rest_framework import  serializers
    
    class 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  models
    
    class 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.data
            return 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):
    		pass
    
        def 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.data
            else:
                # 校验没通过就说明有字段的值是空的,或者长度等不够等,不过我们并没有设置。
                response_msg['status'] = 210
                response_msg['msg'] = '数据校验失败'
                response_msg['errprs'] = book_ser.errors
                
            return Response(response_msg)
    

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

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

  • 序列化类

    from rest_framework import  serializers
    from app01 import  models
    
    class 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的方法,重新赋值修改之后,保存到数据库。

查看效果:

  • image-20230116013040744

    接口截图来源与 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,只有data
        book_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.data
        else:
            response_msg['status']=102
            response_msg['msg']='数据校验失败'
            response_msg['data']=book_ser.errors
        return 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 序列化类的字段类型

BooleanFieldBooleanField()
NullBooleanFieldNullBooleanField()
CharFieldCharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True)
EmailFieldEmailField(max_length=None, min_length=None, allow_blank=False)
RegexFieldRegexField(regex, max_length=None, min_length=None, allow_blank=False)
SlugFieldSlugField(maxlength=50, min_length=None, allow_blank=False) 正则字段,验证正则模式 [a-zA-Z0-9-]+
URLFieldURLField(max_length=200, min_length=None, allow_blank=False)
UUIDFieldUUIDField(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"
IPAddressFieldIPAddressField(protocol=’both’, unpack_ipv4=False, **options)
IntegerFieldIntegerField(max_value=None, min_value=None)
FloatFieldFloatField(max_value=None, min_value=None)
DecimalFieldDecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None) max_digits: 最多位数 decimal_palces: 小数点位置
DateTimeFieldDateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)
DateFieldDateField(format=api_settings.DATE_FORMAT, input_formats=None)
TimeFieldTimeField(format=api_settings.TIME_FORMAT, input_formats=None)
DurationFieldDurationField()
ChoiceFieldChoiceField(choices) choices与Django的用法相同
MultipleChoiceFieldMultipleChoiceField(choices)
FileFieldFileField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)
ImageFieldImageField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)
ListFieldListField(child=, min_length=None, max_length=None)
DictFieldDictField(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()
image-20230116181407382

以下的了解即可:

  • 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 = True
        self.fields['bind_author'].write_only = True

    class Meta:
        model = models.Book
        fields = ['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 ValidationError

class 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 data
        else:
            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 data

class 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 = 100
        self.msg = '成功'

    @property
    def 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')
    
    image-20230117003855433
  • 返回给前端的数据,可以“.”跨表

    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 models

class 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.Book

        fields = ('title','price',)
        #fields = ['title','price']  #也可以是列表

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

# ser.py
class BookSer(ModelSerializer):
    class Meta:
        model = models.Book
        fields = ['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'))


    @property
    def publish_name(self):
        return self.bind_publish.name

    @property
    def author_list(self):
        author_list=self.bind_author.all()
        # 返回对应的用户列表,该列表内的数据为对应作者的姓名和性别。
        return [ {'name':author.name,'sex':author.get_sex_display()}  for author in author_list]

效果展示:

image-20230204230709042

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

七、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 status
    
    return 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父类。

APIViewView的不同之处在于:

  • 传入到视图方法中的是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=Book
        fields='__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=Book
            fields='__all__'
    

视图层:

  • from rest_framework.generics import GenericAPIView
    
    class 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.objects
        serializer_class = BookSerializer
        
        def 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.objects
    serializer_class = BookSerializer
    
    def get(self,request):
        return self.list(request)

    def post(self,request):
        return self.create(request)

class BookDetailView(GenericAPIView,RetrieveModelMixin,DestroyModelMixin,UpdateModelMixin):
    queryset = Book.objects
    serializer_class = BookSerializer
    
    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)
    
# urls.py
    path('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获取全部数据,那么会经过GenericAPIViewListModelMixin扩展类,可调用该扩展类内部的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,DestroyAPIView
    
    
    class BookView(ListAPIView,CreateAPIView):  
        queryset = Book.objects
        serializer_class = BookSerializer
    
    
    class BookDetailView(UpdateAPIView,RetrieveAPIView,DestroyAPIView):
        queryset = Book.objects
        serializer_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方法,都已经封装到了ListAPIViewCreateAPIView中。

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.objects
        serializer_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:

  • 继承了ViewSetMixinAPIView,也可以直接使用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.auth
                self.user, self.auth = user_auth_tuple
                return
        # 如果返回值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 uuid
    
    
    class 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 UserToken

class 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, token
            else:
                raise AuthenticationFailed('认证失败')
        else:
            raise AuthenticationFailed('请求地址中需要携带token')
  • 在认证类中,我们规定了需要在URL地址栏中,添加参数token,值为登陆之后返回给前端的token。

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

    其中,返回的两个值,会被request.userrequest.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.userrequest.auth所接收,其中重点是user**,在权限的校验中,需要获取到当前登陆的用户。(认证组件校验成功之后,扭头就处理权限认证)

11.1 权限组件的使用

基本上与认证大差不差

权限使用步骤:

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

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

一、书写认证代码

from rest_framework.permissions import BasePermission

class UserPermission(BasePermission):
    def  has_permission(self, request, view):

        # 由于认证已经过了,并且返回了用户对象,request.user拿到当前登录用户对象
        user = request.user  
        
		# 在表设计中,User表的user_type字段用于记录用户类型,1为超级用户。
        # 不是超级用户的不可以调用
        if user.user_type==1:
            return True
        else:
            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 SimpleRateThrottle
    
    class 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 DjangoFilterBackend
    
    class 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、没权限、频率上限了,这个时候会直接抛出异常,如:

image-20230131004047195

可以发现并没有携带请求响应状态码,也没有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 Response

class 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属性使用
    @property
    def publish_name(self):
        return self.bind_publish.name

    @property
    def 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 = True
        self.fields['bind_author'].write_only = True

    class 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.objects
    serializer_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.objects
    serializer_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.objects
    serializer_class = ser.BookSer

    # 配置分页
    pagination_class = PageNumberPagination

    def 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   # 指定成子类
    

效果展示:

image-20230208002545875

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' # 取多少条的key
    max_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' # 每一页查询的key
    page_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,但其实APIViewGenericAPIView也是可以使用的,只不过封装程度没有那么多,需要手动书写一些代码,不过换来的是可控性很高。

代码示例:

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_urls
    
    
    urlpatterns = [	
        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):
            pass
    	def post(self, request):
            pass
    	def put(self, request):
            pass
    

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

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

查看效果:

image-20230209034224971

  • 上图中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='图书价格')
    

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

image-20230209034550837
  • 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_token


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

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

接口请求示例:

image-20230211055744048

第三种(自定义):

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

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

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

from rest_framework_jwt.utils import jwt_payload_handler
from rest_framework_jwt.utils import jwt_encode_handler



payload = jwt_payload_handler(user)  # 将获取到的用户对象做成荷载
token = jwt_encode_handler(payload)  # 合成token

20.1.2 校验token

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

请求示例:

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

代码示例:

  • 自定义校验类:

    from rest_framework import exceptions
    
    
    # BaseJSONWebTokenAuthentication继承于drf的BaseAuthentication
    # 这里使用子类BaseJSONWebTokenAuthentication是因为含有authenticate_credentials方法,可从荷载中获取用户对象,而drf原生的认证类并没有。
    class MyToken(BaseJSONWebTokenAuthentication):
        def authenticate(self, request):
            
            # 根据规定从请求头中获取token
            jwt_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",]
    }
    

接口访问示例:

image-20230213022303976
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 IsAuthenticated

from api.auth import MyToken
class BookView(APIView):

    # 使用内置的校验token
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]
    
    def get(self,request):
        print(request.user.email)
        return Response('ok')
    
'''
auth模块中的is_authenticated()方法,就是校验当前用户是否经过校验
同理,内置的IsAuthenticated权限类也是,也是校验当前用户是否经过校验
'''

接口访问示例:

image-20230211062932297
  • 因为使用的是内置的,所以需要 “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'
    }
    

    接口示例:

    image-20230213025905966

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那就尝试着校验,并拿到payload
                    payload = 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_value
            
            raise  AuthenticationFailed('没有多携带认证信息')
    

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

    from api.models import User
    
    print(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_value
    
            raise  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页面:

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

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

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

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

代码示例:

  • 路由

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

    from rest_framework.viewsets import ViewSet
    
    class 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 return
            return 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.User
            fields=['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_password
                if user.check_password(password):
                    # 签发token
                    payload = jwt_payload_handler(user)  # 把user传入,得到payload
                    token = jwt_encode_handler(payload)  # 把payload传入,得到token
                    
                    # 可以给序列化对象以属性添加的方式,将token返回给视图层
                    self.context['token']=token
                    self.context['username']=user.username
                    
                    # 根据全局钩子函数,这里需要返回attrs
                    return attrs
                else:
                    raise ValidationError('密码错误')
            else:
                raise ValidationError('用户不存在')
    

接口请求示例:
image-20230216041210994

20.4 token过期时间

token过期时间:

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

二十一、 RBAC

作为补充知识点

RBAC:

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

内有:

  • user表,group表,permission表

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

应用场景:

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

2023 - 02 - 10

自己目前遇到的问题:

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

二十二、django缓存

更多详情见官方文档:https://docs.djangoproject.com/zh-hans/4.1/topics/cache/

缓存介绍:

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

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

  • 缓存是将一些常用的数据保存内存或者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一般与其他缓存配合使用

22.1 前后端混合开发缓存的使用

前后端混合开发缓存的使用:

  • 一、缓存的位置,是通过配置文件来操作,还是数据库、
  • 二、缓存的粒度,是**全站缓存;还是单页面缓存;还是页面局部缓存**。

全站缓存

  • 由于请求过来和回去,都需要对缓存进行存储、获取、更新等操作,所以配置中间件即可,这里不需要自定义,直接用内置的。

    MIDDLEWARE = [
        'django.middleware.cache.UpdateCacheMiddleware',
    	...其他中间件略...
    	'django.middleware.cache.FetchFromCacheMiddleware',
    ]
    
    CACHE_MIDDLEWARE_SECONDS=10  # 全站缓存时间,单位为秒。
    

    注意前后顺序不要倒乱。

单页面缓存

  • 由于是全栈开发,视图类直接返回页面,所以可以给视图类添加装饰器,装饰器内部针对页面进行缓存,这个也不需要自定义,内置写好了。

    from django.views.decorators.cache import cache_page
    
    @cache_page(5)  # 缓存时间,单位:秒。
    def test_cache(request):
        import time
        ctime = time.time()
        # 刷新页面,查看5秒内时间是否还会有所变化,如果没有那就说明数据是从缓存中取的。
        return render(request, 'index.html', context={'ctime': ctime})
    

页面局部缓存

  • 模板语法过滤器即可

    { % load cache %}
    { % cache 5 'name' %}  # 5表示5s钟,name是用来存储缓存的唯一key值
    {{ctime}}
    { % endcache %}
    

22.2 前后端分离缓存的使用

由于是前后端分离,django后端在接收到请求后,先去缓存中进行查询,如果有直接返回,如果没有,就再去连表查询,查询数据拿到之后,返回的同时缓存数据。

所以,前后端分离的项目,如果想要使用缓存,那么获取缓存和添加缓存就需要我们掌握使用。

cache.set 添加缓存cache.get 获取缓存

from django.core.cache import cache

cache.set('key',value,超时时间)  # value可以是任意数据类型。超时时间单位:秒
cache.get('key','未查询到返回的默认值')

更多用法见官方文档:https://docs.djangoproject.com/zh-hans/4.1/topics/cache/

个人是觉得,set方法的value可以是任意数据类型,那么就可以是对象,有了对象就可以实现很多效果,那么cache的其他方法就显得不那么重要,而且set方法可以设置超时时间,比手动用删除命令要好不少。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿煜酱~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值