Django-16:rest-framework与jwt
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:返回完整的资源对象
- https://api.test.com/collection:返回资源对象的列表(数组)
十、需要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.......
在APIView的dispatch方法中,先对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
andwrite_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方法来调用序列化器,并传入data、instance等参数并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作为三大认证的一环,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相关推荐
- Akka-CQRS(16)- gRPC用JWT进行权限管理
前面谈过gRPC的SSL/TLS安全机制,发现设置过程比较复杂:比如证书签名:需要服务端.客户端两头都设置等.想想实际上用JWT会更加便捷,而且更安全和功能强大,因为除JWT的加密签名之外还可以把私密 ...
- Django前后端分离1——jwt
一.前戏 1.base64 方法 作用 参数 返回值 b64encode 将输入的参数转化为base64规则的串 预加密的明文,类型为bytes:例:b'guoxiaonao' base64对应编码的 ...
- Django REST framework 认证、权限和频率组件
认证与权限频率组件 身份验证是将传入请求与一组标识凭据(例如请求来自的用户或其签名的令牌)相关联的机制.然后 权限 和 限制 组件决定是否拒绝这个请求. 简单来说就是: 认证确定了你是谁 权限确定你能 ...
- Django JWT认证实现
配置JWT认证 先通过 pip install djangorestframework 命令下载 Django REST framework 库,再通过 pip install djangorestf ...
- Django Python Web应用程序框架简介
在这个由四部分组成的系列文章的前三篇文章中,比较了不同的Python Web框架,我们介绍了Pyramid , Flask和Tornado Web框架. 我们已经构建了同一个应用程序3次,最终进入了D ...
- Django前后端分离实现登录验证码功能
Django前后端分离实现登录验证码功能 当下最流行最热门的开发方式当属前后端分离开发,分工也更加明确与专注,前端也是越来越难,几天不学习就跟不上节奏,一个月不学习可以好不夸张的说,你已经不适合这个行 ...
- Django Python:完整的BUNDLE + Django真实项目2021
Django和python Bundle:从学习python的所有基础知识到高级python再到UI设计TKINTER,然后是Django 你会学到: 学习编程的基础知识 学习Python编程语言 学 ...
- python主要用于后端开发还是前端,Django是用于前端还是后端?
I often see people claiming their backend is implemented in Django, but isn't Django supposed to be ...
- REST framework 基本使用
安装 pip install djangorestframework djangorestframework 介绍 djangorestframework 主要使用 APIView,其实APIView ...
- 小白必看!Django 模板语言基础来啦
作者 | 单雨 责编 | 胡巍巍 出品 | CSDN(ID:CSDNnews) 前言 为了实现模板封装和复用,提高HTML界面调试便捷性以及前后端解耦等目标,Django定义了自己的网络模板语言. 当 ...
最新文章
- CentOS 7.8下安装PyRosetta4
- c++找不到标识符_沪C转沪牌流程攻略大全
- Smartforms Debug
- JAVA版StarDict星际译王简单实现
- Revit API创建标高,单位转换
- 风机桨叶故障诊断(三) 识别桨叶——初步构建BP神经网络
- 麻省理工学院推出数据美国大数据可视化工具
- 计算机专业认证协会,2017计算机类专业认证委员会工作总结会在京召开
- VS201x的项目属性配置
- 没想到你是这样的MethodArgumentNotValidException
- 租用游艇问题(pta)
- flash在线视频播放器
- 愤世嫉俗的程序员,总在某乎发表言论,当起了“键盘侠”
- 二叉树的递归遍历(树UVa548 紫书p155)
- linux中cmd是什么文件格式,CMD 文件扩展名: 它是什么以及如何打开它?
- ssh推送Warning: Permanently added 'gitee.com,120.55.226.24' (ECDSA) to the list of known ho
- 劳动保障协管员计算机考试,劳动保障协管员考试试题
- 进入IT互联网行业一定要报培训班嘛?
- 肿瘤与癌症检测相关产品的生物信息分析
- 把金庸小说数据化——关于语言的思一点考