DRF
DRF
(1)API接口
-
前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。
-
为了在团队内部形成共识、防止个人习惯差异引起的混乱,我们需要找到一种大家都觉得很好的接口实现规范,而且这种规范能够让后端写的接口,用途一目了然,减少双方之间的合作成本。
-
通过网络,规定了前后台信息交互规则的url链接,也就是前后台信息交互的媒介
-
Web API接口和一般的url链接还是有区别的,Web API接口简单概括有下面四大特点
-
url:长得像返回数据的url链接
-
请求方式:get、post、put、patch、delete
- 采用get方式请求上方接口
-
请求参数:json或xml格式的key-value类型数据
- ak:6E823f587c95f0148c19993539b99295
- region:上海
- query:肯德基
- output:json
-
响应结果:json或xml格式的数据
-
上方请求参数的output参数值决定了响应数据的格式
-
数据
# xml格式 https://api.map.baidu.com/place/v2/search?ak=6E823f587c95f0148c19993539b99295®ion=%E4%B8%8A%E6%B5%B7&query=%E8%82%AF%E5%BE%B7%E5%9F%BA&output=xml #json格式 https://api.map.baidu.com/place/v2/search?ak=6E823f587c95f0148c19993539b99295®ion=%E4%B8%8A%E6%B5%B7&query=%E8%82%AF%E5%BE%B7%E5%9F%BA&output=json { "status":0, "message":"ok", "results":[ { "name":"肯德基(罗餐厅)", "location":{ "lat":31.415354, "lng":121.357339 }, "address":"月罗路2380号", "province":"上海市", "city":"上海市", "area":"宝山区", "street_id":"339ed41ae1d6dc320a5cb37c", "telephone":"(021)56761006", "detail":1, "uid":"339ed41ae1d6dc320a5cb37c" } ... ] }
-
-
API接口案例,继承View
class TaskView(View): def get(self, request): res = Task.objects.all() list = [] for i in res: data = { 'id': i.id, 'task_id': i.task_id, 'task_name': i.task_name, 'task_time': i.task_time.strftime('%Y-%m-%d %H:%M:%S'), 'task_desc': i.task_desc, } list.append(data) return JsonResponse({'code': 200, 'msg': '查询所有数据成功', 'data': list}, json_dumps_params={'ensure_ascii': False}) def post(self, request): res = json.loads(request.body) task_id = res.get('task_id') task_name = res.get('task_name') task_time = res.get('task_time') task_desc = res.get('task_desc') data = { 'task_id': task_id, 'task_name': task_name, 'task_time': task_time, 'task_desc': task_desc, } Task.objects.create(task_id=task_id, task_name=task_name, task_time=task_time, task_desc=task_desc) return JsonResponse({'code': 200, 'msg': '添加数据成功', 'data': data}, json_dumps_params={'ensure_ascii': False}) class TaskDetailView(View): def get(self, request, pk): res = Task.objects.filter(id=pk).first() if not res: return JsonResponse({'code': 404, 'msg': '查询失败,数据不存在'}, json_dumps_params={'ensure_ascii': False}) else: data = { 'id': res.id, 'task_id': res.task_id, 'task_name': res.task_name, 'task_time': res.task_time.strftime('%Y-%m-%d %H:%M:%S'), 'task_desc': res.task_desc, } return JsonResponse({'code': 200, 'msg': '查询单条数据成功', 'data': data}, json_dumps_params={'ensure_ascii': False}) def delete(self, request, pk): task_obj = Task.objects.filter(id=pk) if not task_obj: return JsonResponse({'code': 404, 'msg': '删除失败,数据不存在'}, json_dumps_params={'ensure_ascii': False}) else: task_obj.delete() return JsonResponse({'code': 200, 'msg': '删除数据成功'}, json_dumps_params={'ensure_ascii': False}) def put(self, request, pk): res = json.loads(request.body) task_id = res.get('task_id') task_name = res.get('task_name') task_time = res.get('task_time') task_desc = res.get('task_desc') task_obj = Task.objects.filter(id=pk) task_obj.update(task_id=task_id, task_name=task_name, task_time=task_time, task_desc=task_desc) return JsonResponse({'code': 200, 'msg': '修改数据成功'}, json_dumps_params={'ensure_ascii': False})
(2)body的编码格式
# 1 get 请求,可以在地址栏中带数据---》也能在body体中带数据 -注意:可以提交到后端,放在request.body中取 # 2 urlencoded 编码格式---》请求体中--》name=heart&age=38 # 3 form-data格式:携带文件和数据 -文件从:request.FILES.get('myfile') -数据:request.POST.get() -request.body 能不能用,取决于文件大小,一般不用打印body(会报错) # 4 json格式 -request.POST 是没有数据的 -request.body中有---》需要自己转
(3)restful API规范
-
REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征性状态转移)。 它首次出现在2000年Roy Fielding的博士论文中。
-
RESTful是一种定义Web API接口的设计风格,尤其适用于前后端分离的应用模式中。
-
这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。
-
事实上,我们可以使用任何一个框架都可以实现符合restful规范的API接口。
(1)数据的安全保障
-
url链接一般都采用https协议进行传输
注:采用https协议,可以提高数据交互过程中的安全性
(2)接口特征表现
-
用api关键字标识接口url:
注:看到api字眼,就代表该请求url链接是完成前后台数据交互的
(3)多数据版本共存
-
在url链接中标识数据版本
注:url链接中的v1、v2就是不同数据版本的体现(只有在一种数据资源有多版本情况下)
(4)数据即是资源,均使用名词(可复数)
-
接口一般都是完成前后台数据的交互,交互的数据我们称之为资源
注:一般提倡用资源的复数形式,在url链接中奖励不要出现操作资源的动词,错误示范:https://api.baidu.com/delete-user
-
特殊的接口可以出现动词,因为这些接口一般没有一个明确的资源,或是动词就是接口的核心含义
(5)资源操作由请求方式决定(method)
- 操作资源一般都会涉及到增删改查,我们提供请求方式来标识增删改查动作
- 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 - patch请求:局部修改主键为1的书
- https://api.baidu.com/books/1 - delete请求:删除主键为1的书
(6)过滤,通过在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:指定筛选条件
(7)响应状态码
[1]正常响应
- 响应状态码2xx
- 200:常规请求
- 201:创建成功
[2]重定向响应
- 响应状态码3xx
- 301:永久重定向
- 302:暂时重定向
[3]客户端异常
- 响应状态码4xx
- 403:请求无权限
- 404:请求路径不存在
- 405:请求方法不存在
[4]服务器异常
- 响应状态码5xx
- 500:服务器异常
(8)错误处理,应返回错误信息,error当做key
{ error: "无权限操作" }
(9)返回结果,针对不同操作,服务器向用户返回的结果应该符合以下规范
GET /collection:返回资源对象的列表(数组) GET /collection/resource:返回单个资源对象 POST /collection:返回新生成的资源对象 PUT /collection/resource:返回完整的资源对象 PATCH /collection/resource:返回完整的资源对象 DELETE /collection/resource:返回一个空文档
(10)需要url请求的资源需要访问资源的请求链接
# Hypermedia API,RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么 { "status": 0, "msg": "ok", "results":[ { "name":"肯德基(罗餐厅)", "img": "https://image.baidu.com/kfc/001.png" } ... ] }
- 比较好的接口返回
# 响应数据要有状态码、状态信息以及数据本身 { "status": 0, "msg": "ok", "results":[ { "name":"肯德基(罗餐厅)", "location":{ "lat":31.415354, "lng":121.357339 }, "address":"月罗路2380号", "province":"上海市", "city":"上海市", "area":"宝山区", "street_id":"339ed41ae1d6dc320a5cb37c", "telephone":"(021)56761006", "detail":1, "uid":"339ed41ae1d6dc320a5cb37c" } ... ] }
(4)APIView
- 只要继承了APIView,就不用处理csrf
- 多一个
request.data
request.data
:request.data
是 DRF 中的一个特殊属性,它是一个类字典对象,用于表示解析后的请求数据。- DRF 会根据请求的 Content-Type 自动解析请求体中的数据,并将其转换为适当的 Python 数据类型(如字典、列表等),然后存储在
request.data
中。 request.data
中的数据已经被 DRF 处理和验证过,可以直接在视图中使用。- 通常在编写 DRF 视图时,我们更倾向于使用
request.data
来访问请求数据,因为它提供了更高级的抽象和处理。
request.body
:request.body
是 Django HttpRequest 对象的属性,用于获取原始的请求体数据,即未经过任何解析的原始字节字符串。- 它是一个字节字符串,包含了 HTTP 请求中的原始数据,不论是 JSON、XML、Form 表单数据还是其他格式。
request.body
中的数据是未经处理的原始数据,需要手动进行解析和处理。在处理非标准的请求格式时,可能会使用request.body
。
总结
request.data
是 DRF 提供的对请求数据进行解析和处理后的高级抽象request.body
则是原始的请求体数据,需要手动解析。
(1)基于APIView编写接口
- 基于APIView编写接口,继承APIView类
from rest_framework.views import APIView class TaskView(APIView): def format_data(self, request): dic = {} for k, v in request.data.items(): dic[k] = v return dic def get(self, request): res = Task.objects.all() list = [] for i in res: data = { 'id': i.id, 'task_id': i.task_id, 'task_name': i.task_name, 'task_time': i.task_time.strftime('%Y-%m-%d %H:%M:%S'), 'task_desc': i.task_desc, } list.append(data) return JsonResponse({'code': 200, 'msg': '查询所有数据成功', 'data': list}, json_dumps_params={'ensure_ascii': False}) def post(self, request): data = self.format_data(request) Task.objects.create(**data) return JsonResponse({'code': 200, 'msg': '添加数据成功'}, json_dumps_params={'ensure_ascii': False}) class TaskDetailView(APIView): def format_data(self, request): dic = {} for k, v in request.data.items(): dic[k] = v return dic def get(self, request, pk): res = Task.objects.filter(id=pk).first() if not res: return JsonResponse({'code': 404, 'msg': '查询失败,数据不存在'}, json_dumps_params={'ensure_ascii': False}) else: data = { 'id': res.id, 'task_id': res.task_id, 'task_name': res.task_name, 'task_time': res.task_time.strftime('%Y-%m-%d %H:%M:%S'), 'task_desc': res.task_desc, } return JsonResponse({'code': 200, 'msg': '查询单条数据成功', 'data': data}, json_dumps_params={'ensure_ascii': False}) def delete(self, request, pk): task_obj = Task.objects.filter(id=pk) if not task_obj: return JsonResponse({'code': 404, 'msg': '删除失败,数据不存在'}, json_dumps_params={'ensure_ascii': False}) else: task_obj.delete() return JsonResponse({'code': 200, 'msg': '删除数据成功'}, json_dumps_params={'ensure_ascii': False}) def put(self, request, pk): dic = self.format_data(request) Task.objects.filter(id=pk).update(**dic) return JsonResponse({'code': 200, 'msg': '修改数据成功'}, json_dumps_params={'ensure_ascii': False})
(2)APIView的执行流程
# 1 APIView继承了 Django的View---》 class APIView(View) # 2 请求来了,路由匹配成功后---》执行流程 2.1 路由配置 path('books/', BookView.as_view()), 2.2 BookView.as_view()(request)-->BookView中没有as_view--》找父类APIView的as_view BookView-->APIView-->View 2.3 APIView的as_view @classmethod # 绑定给类的方法,类来调用 def as_view(cls, **initkwargs): # super代指父类--》父类是View--》之前读过--》self.dispatch() # 这个view 还是原来View的as_view的执行结果--》as_view中有个view内部函数 view = super().as_view(**initkwargs) # 只要继承了APIView,不需要处理csrf ''' @csrf_exempt def index(request): pass 等同于 index=csrf_exempt(index) 以后调用index,其实调用的 是csrf_exempt(index)() ''' return csrf_exempt(view) 2.4 请求来了,真正执行的是:csrf_exempt(view)(request)-->去除了csrf的view(request)--》self.dispatch() 2.5 请求来了,真正执行的是 self.dispatch(request)--->self 是 视图类的对象 BookView的对象--》自己没有--》APIView中 2.6 现在要看 APIView的dispatch def dispatch(self, request, *args, **kwargs): # 1 包装了新的request对象---》现在这个requets对象,已经不是原来django的request对象了 request = self.initialize_request(request, *args, **kwargs) try: # 2 APIView的initial--》三件事:三大认证:认证,频率,权限 self.initial(request, *args, **kwargs) # 3 就是执行跟请求方式同名的方法 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) # 4 如果在三大认证或视图类的方法中出了异常,会被统一捕获处理 except Exception as exc: response = self.handle_exception(exc) self.response = self.finalize_response(request, response, *args, **kwargs) return self.response
执行流程总结
- 只要继承了APIView,就没有csrf限制了
- 只要继承了APIView,request就是新的request了,有request.data
- 在执行跟请求方式同名的方法之前,执行了三大认证:认证,频率,权限
- 只要在三大认证或者视图类的方法中出了异常,都会被捕获,统一处理
(3)Request对象
-
APIView的request对象变成了新的
-
老的是
django.core.handlers.wsgi.WSGIRequest
类的对象 -
现在是
rest_framework.request.Request
类的对象 -
新的request支持之前所有老request的操作,多了一个
request.data
和request.query_params(等同于老的request.GET)
- request.query_params能获取的参数是url中问号后携带的关键词所对应的数据
- http://example.com/api/resource/?param1=value1¶m2=value2
-
request._request
就是老的request
-
request.user
通过了认证类,当前登录用户
(5)序列化
-
api接口开发,最核心最常见的一个过程就是序列化,所谓序列化就是把数据转换格式,序列化可以分两个阶段:
-
序列化: 把我们识别的数据转换成指定的格式提供给别人。
-
例如:我们在django中获取到的数据默认是模型对象,但是模型对象数据无法直接提供给前端或别的平台使用,所以我们需要把数据进行序列化,变成字符串或者json数据,提供给别人。
-
反序列化:把别人提供的数据转换/还原成我们需要的格式。
-
例如:前端js提供过来的json数据,对于python而言就是字符串,我们需要进行反序列化换成模型类对象,这样我们才能把数据保存到数据库中。
(6)序列化类
- 借助drf提供的序列化类
- 快速序列化
- 反序列化之前的数据校验
- 反序列化
(1)使用步骤
- 写一个类,继承
Serialier
from rest_framework import serializers class TaskSerializer(serializers.Serializer):
- 在类中写字段(instance/data/many),字段就是要序列化的字段
from rest_framework import serializers class TaskSerializer(serializers.Serializer): task_id = serializers.CharField() task_name = serializers.CharField() task_time = serializers.DateTimeField() task_desc = serializers.CharField()
-
在视图函数中,序列化类,实例化得到对象,传入该传的参数
-
两个重要参数
instance
里面放要序列化的queryset对象,如果是多条数据,必须加many=True
data
里面放要校验的数据(request.data)
-
使用序列化类对象的
serializer.data
方法完成序列化
def get(self, request): res = Task.objects.all() serializer = TaskSerializer(instance=res,many=True) return Response({'data': serializer.data})
(2)校验数据
-
serializer.is_valid()
-
要校验前端传入的数据
-
要加入字段参数,控制数据校验
# 最大长度为8,最小长度为3 task_id = serializers.CharField(max_length=8,min_length=3)
- 校验信息
serializer.errors
return Response({'code': 200, 'msg': serializer.errors})
- 如果要显示中文,先汉化,在settings里配置
INSTALLED_APPS = [ ... 'rest_framework' ] LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = False
- 局部钩子函数
- validate_字段名(self,字段名)
from rest_framework.exceptions import ValidationError
def validate_task_name(self, task_name): if task_name.startswith('sb'): raise ValidationError('task_name不能以sb开头') else: return task_name
-
全局钩子
-
validate(self,attrs)
-
attrs是前端传入,经过字段自己校验和局部钩子校验都通过的数据(字典)
def validate(self, attrs): if attrs.get('task_name') == attrs.get('task_desc'): raise ValidationError('task_name和task_desc不能相同') else: return attrs
(3)反序列化数据保存
serializer.save()
需要在序列化类中重写create才能存进去- 一定要返回
def post(self, request): serializer =TaskSerializer(data=request.data) if serializer.is_valid(): serializer.save() # 重写create 触发序列化中的create return Response({'code': 200, 'msg': '保存成功'}) else: return Response({'code': 200, 'msg': serializer.errors})
def create(self, validated_data): # validated_data : 前端传入,所有校验通过的数据(字典) task = Task.objects.create(**validated_data) return task
(4)反序列化数据修改
- 必须传
data
和instance
- serializer类会判断
instance
是否为None,如果是,就会调用创建,如果不是就会调用修改
def put(self, request, pk): obj = Task.objects.filter(id=pk).first() serializer = TaskSerializer(instance=obj, data=request.data) if not obj: return Response({'code': 200, 'msg': '修改数据失败'}) if serializer.is_valid(): serializer.save() # 重写update 触发序列化中的update return Response({'code': 200, 'msg': '修改数据成功'}) else: return Response({'code': 200, 'msg': serializer.errors})
def update(self, instance, validated_data): # instance : 要修改的对象 # validated_data : 前端传入,数据 # 使用反射将前端传入的数据赋值给instance对象 for k, v in validated_data.items(): setattr(instance, k, v) instance.save() # 更改保存到数据库中 return instance
(5)常用字段类型
- 其他都和models中的一一对应
- 多出了
ListField()
和DictField()
字段 | 字段构造方式 |
---|---|
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=) |
(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页面时,显示的字段帮助提示信息 |
- validate案例
from rest_framework.exceptions import ValidationError def validate_id(id): if id.startswith('gg'): raise ValidationError('不能以gg开头') else: return id class TaskSerializer(serializers.Serializer): task_id = serializer.CharField(max_length=32,validators=[validate_id])
- error_messages案例
class TaskSerializer(serializers.Serializer): task_id = serializer.CharField(max_length=32,error_messages={'max_length':'太长了'})
(7)多表关联序列化
(1)定制返回字段名之source
- serializer.py
class BookSerializer(serializers.Serializer): price = serializers.IntegerField(source='book_price') publish = serializers.CharField(source="publish.pub_name") publish_id = serializers.CharField(source='publish.id') # 注意:这里有个坑,前后不能一致,会报错,如下所示: # book_price = serializers.IntegerField(source='book_price')
- models.py
class Book(models.Model): book_price = models.IntegerField() class Publish(models.Model): pub_name = models.CharField(max_length=32)
(2)定制返回字段
后期想实现如下格式返回的数据:
{name:书名,price:价格,publish:{name:出版社名,addr:地址},authors:[{},{}]}
DictField()
和ListField()
- 方式一:在表模型中定义方法
serializer.py
class BookSerializer(serializers.Serializer): publish_detail = serializers.DictField() author_detail = serializers.ListField()
models.py
class Book(models.Model): publish = models.ForeignKey('Publish', on_delete=models.CASCADE) author = models.ManyToManyField('Authors') def publish_detail(self): return {"name": self.publish.pub_name, "pub_city": self.publish.pub_city} def author_detail(self): l = [] for author in self.author.all(): l.append({"name": author.author_name, "age": author.author_age}) return l # 也可以直接写 def publish_name(self) return self.name
(3)定制返回格式之SerializerMethodField
serializers.SerializerMethodField()
- 一定要配合一个方法,必须叫
get_字段名
serializer.py
class BookSerializer(serializers.Serializer): publish_detail = serializers.SerializerMethodField() def get_publish_detail(self, obj): # obj 就是当前序列化到的book对象 return { 'pub_name': obj.publish.pub_name, 'pub_city': obj.publish.pub_city, 'id': obj.pk } author_detail = serializers.SerializerMethodField() def get_author_detail(self, obj): # obj 就是当前序列化到的book对象 l = [] for author in obj.author.all(): l.append({"id": author.pk, 'author_name': author.author_name, 'author_age': author.author_age}) return l
(4)定制返回格式之子序列化
serializer.py
如果是多条字段的话,要加many=True
class PublishSerializer(serializers.Serializer): id = serializers.IntegerField() pub_name = serializers.CharField() pub_city = serializers.CharField() pub_email = serializers.EmailField() class AuthorSerializer(serializers.Serializer): id = serializers.IntegerField() author_name = serializers.CharField() author_age = serializers.IntegerField() class BookSerializer(serializers.Serializer): publish_detail = PublishSerializer(source='publish') author_detail = AuthorSerializer(source='author', many=True)
models.py
class Publish(models.Model): pub_name = models.CharField(max_length=32) pub_city = models.CharField(max_length=32) pub_email = models.EmailField()
- 如果要改返回前端的字段名,需要加上source
publish_detail = PublishSerializer(source='publish')
(8)多表关联反序列化保存
- 反序列化字段可以随意命名,和表字段没关系,但是后续保存和修改要对应好
serializer.py
class BookSerializer(serializers.Serializer): publish = serializers.IntegerField(write_only=True) author = serializers.ListField(write_only=True) def create(self, validated_data): publish_id = validated_data.pop('publish') # 1 author = validated_data.pop('author') book = Book.objects.create(**validated_data, publish_id=publish_id) book.author.add(*author) return book def update(self, instance, validated_data): publish_id = validated_data.pop('publish') author = validated_data.pop('author') book_obj = Book.objects.filter(id=instance) book_obj.update(**validated_data, publish_id=publish_id) book_obj.first().author.set(author) return book_obj.first()
views.py
class BookDetailView(APIView): def post(self, request): serializer = BookSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response({'code': 200, 'msg': '数据保存成功'}) else: return Response({'code': 200, 'msg': serializer.errors}) def put(self, request, pk): serializer = BookSerializer(instance=pk,data=request.data) if serializer.is_valid(): serializer.save() return Response({'code': 200, 'msg': '修改数据成功', 'data': serializer.data})
(9)ModelSerializer使用
model
是要传入的表fields
是要序列化的字段fields='__all__'
是模型表中所有的字段- 如果要指定字段序列化 应该改成
fields=['','']
- extra_kwargs里传入的是需要添加的限制条件
- 如果序列化缺少字段,自己手动添加
read_only
class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = '__all__' # 需要反序列化的字段 extra_kwargs = { 'book_name':{'max_length':8}, 'publish': {'write_only': True}, 'author': {'write_only': True}, } # 序列化缺少的字段 publish_detail = PublishSerializer(source='publish', read_only=True) author_detail = AuthorSerializer(source='author', many=True, read_only=True)
总结
- 不用重写create和update了,但是所有的字段名必须一一对应
- 局部钩子和全局钩子和之前一模一样,要写在序列化类中
- 要修改模型表中的字段也是可以的,在子序列化中直接修改即可,然后添加上参数source=''
(10)DRF之请求
- 继承了APIView之后,请求可以是urlencoded,form-data,json格式的数据
- request.data 都能取出请求体的数据(QueryDict)
- 那么要只让取一种格式的数据,怎么做?解决方法如下
(1)方式一
-
from rest_framework.parsers import ...
-
parser_classes = []
-
在视图类中控制
from rest_framework.parsers import JSONParser, FormParser, MultiPartParser class BookView(APIView): parser_classes = [JSONParser] def post(self, request): print(request.data) return Response('ok')
(2)方式二
- 在
settings.py
中控制 - 全局生效,所有视图类都受影响
- 如果全局配置了,还想局部再控制,就直接在视图类中定义
parser_classes = [JSONParser,...]
,会优先以视图类为准
REST_FRAMEWORK = { 'DEFAULT_PARSER_CLASSES': [ 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', 'rest_framework.parsers.MultiPartParser', ], }
(11)DRF之响应
- 响应对象:DRF提供的Response,以后返回都用它。
- 必须注册app
INSTALLED_APPS = [ ... 'rest_framework' ]
(1)Response参数分析
- data 数据
return Response(data={})
- status 状态码 也可以直接写数字
from rest_framework.status import HTTP_200_OK
- headers 响应头数据
(2)响应格式
-
from rest_framework.renderers import ...
-
renderer_classes = []
-
DRF接口会根据客户端类型自动返回不同格式,浏览器返回HTML,其他返回JSON格式
-
统一返回json格式,类似与解析
from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer class BookView(APIView): renderer_classes = [JSONRenderer]
- 在配置文件中配置,局部再使用也是再在里面写,优先视图类中的配置
REST_FRAMEWORK = { 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.TemplateHTMLRenderer', ] }
(12)2个视图基类
(1)APIView
from rest_framework.views import APIView
(2)GenericAPIView
-
继承了APIView
-
需要定义两个属性
queryset = 模型表.objects.all()
查询所有的数据serializer_class = 序列化类
定义序列化类
-
三个方法,获取数据和实例化序列化对象
self.get_queryset()
获取所有self.get_object()
获取单条self.get_serializer()
实例化序列化类self.get_serializer_class()
确定视图应该使用哪个序列化器类来序列化或反序列化数据
from rest_framework.generics import GenericAPIView class BookView(GenericAPIView): queryset = Book.objects.all() # 查询所有的数据 serializer_class = BookSerializer # 序列化类 def get(self, request): obj = self.get_queryset() # 使用 get_queryset()方法获取所有数据,而不要使用self.queryset属性获取 serializer = self.get_serializer(instance=obj, many=True) # 使用序列化类 直接使用方法 return Response({'code': 200, 'msg': '查询所有数据成功', 'data': serializer.data})
(13)5个视图扩展类
-
超一形态
-
必须继承
GenericAPIView
from rest_framework.mixins import CreateModelMixin,RetrieveModelMixin,DestroyModelMixin,ListModelMixin,UpdateModelMixin
-
ListModelMixin
: 查询所有 -
CreateModelMixin
: 新增一条 -
RetrieveModelMixin
: 查询一条 -
DestroyModelMixin
: 删除一条 -
UpdateModelMixin
: 更新一条
from rest_framework.generics import GenericAPIView from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, DestroyModelMixin, ListModelMixin,UpdateModelMixin class BookView(GenericAPIView, CreateModelMixin, ListModelMixin): queryset = Book.objects.all() # 查询所有的数据 serializer_class = BookSerializer # 序列化类 def get(self, request): return super().list(request) # ListModelMixin的list def post(self, request): return super().create(request) # CreateModelMixin的create class BookDetailView(GenericAPIView, RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin): queryset = Book.objects.all() # 查询所有的数据 serializer_class = BookSerializer # 序列化类 def get(self, request, pk): return super().retrieve(request) # RetrieveModelMixin的retrieve def post(self, request, pk): return super().destroy(request) # DestroyModelMixin的destroy def put(self, request, pk): return super().update(request) # UpdateModelMixin的update
(1)9个视图子类
-
超二形态
-
等于是把基类和扩展类结合起来了,每个继承的都是视图扩展类和基类
-
写了就不用再写get post方法了
-
五大类
from rest_framework.generics import ListAPIView,RetrieveAPIView,UpdateAPIView,DestroyAPIView,CreateAPIView
-
ListAPIView
: 查询所有- (继承mixins.ListModelMixin,GenericAPIView)
-
RetrieveAPIView
: 查询一条- (继承mixins.RetrieveModelMixin,GenericAPIView)
-
UpdateAPIView
: 修改一条- (继承mixins.UpdateModelMixin,GenericAPIView)
-
DestroyAPIView
: 删除一条- (继承mixins.DestroyModelMixin,GenericAPIView)
-
CreateAPIView
: 增加一条- (继承mixins.CreateModelMixin,GenericAPIView)
-
四小类
from rest_framework.generics import ListCreateAPIView,RetrieveUpdateDestroyAPIView,RetrieveDestroyAPIView,RetrieveUpdateAPIView
-
ListCreateAPIView
: 查询所有和创建一条- (继承mixins.ListModelMixin,mixins.CreateModelMixinGenericAPIView)
-
RetrieveUpdateDestroyAPIView
: 查询一条、修改、删除- (继承mixins.RetrieveModelMixin,mixins.UpdateModelMixin,mixins.DestroyModelMixin,GenericAPIView)
-
RetrieveDestroyAPIView
: 查询一条、删除- (继承mixins.RetrieveModelMixin,mixins.DestroyModelMixin,GenericAPIView)
-
RetrieveUpdateAPIView
: 查询一条、修改- (继承mixins.RetrieveModelMixin,mixins.UpdateModelMixin,GenericAPIView)
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView, RetrieveDestroyAPIView,RetrieveUpdateAPIView class BookView(ListCreateAPIView): queryset = Book.objects.all() serializer_class = BookSerializer class BookDetailView(RetrieveUpdateDestroyAPIView): queryset = Book.objects.all() serializer_class = BookSerializer
(14)视图集
-
后期如果要写五个接口,通过一个类实现
-
一旦使用视图集,路由写法就变了
-
from rest_framework.viewsets import ModelViewSet
urls.py
urlpatterns = [ path('book/', BookView.as_view({'get': 'list', 'post': 'create'})), path('book/<int:pk>', BookView.as_view({'get': 'retrieve', 'delete': 'destroy', 'put': 'update'})), ]
views.py
from rest_framework.viewsets import ModelViewSet class BookView(ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer
(1)定制返回格式
- 重写对应方法
class BookView(ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer def list(self, request, *args, **kwargs): res = super().list(request, *args, **kwargs) print(res) # <Response status_code=200, "text/html; charset=utf-8"> print( res.data) # [{'id': 1, 'name': '蝴蝶2', 'price': '222', 'publish': '浙江出版社'}, {'id': 2, 'name': 'talktalkendless', 'price': '333', 'publish': '福建出版社'}] return Response({'code': 200, 'msg': '查询多条成功', 'data': res.data}) """ { "code": 200, "msg": "查询多条成功", "data": [ { "id": 1, "name": "蝴蝶2", "price": "222", "publish": "浙江出版社" }, { "id": 2, "name": "talktalkendless", "price": "333", "publish": "福建出版社" } ] } """
(2)ModelViewSet源码
- 视图类:继承了APIView-->
GenericAPIView
- 继承了
五个扩展类
- 继承了
GenericViewSet
- GenericViewSet :
ViewSetMixin
+GenericAPIView
- ViewSetMixin : 核心,只要继承它,路由写法就变了,必须加action做映射,视图类中,可以写任意名的方法,只要做好映射,就能执行
views.py
from rest_framework.viewsets import ViewSetMixin class BookView(ViewSetMixin,GenericAPIView): queryset = Book.objects.all() serializer_class = BookSerializer def login(self,request): return Response('ok') # "ok"
urls.py
urlpatterns = [ path('book/', BookView.as_view({'get': 'login'})), ]
(3)ViewSetMixin源码
- 通过重写as_view使得路由写法变了
@classonlymethod def as_view(cls, actions=None, **initkwargs): # 0 跟APIView的as_view差不多 # 1 actions must not be empty,如果为空抛异常 # 2 通过反射把请求方式同名的方法放到了视图类中--》对应咱们的映射 def view(request, *args, **kwargs): self = cls(**initkwargs) # actions 咱们传入的字典--》映射关系 # {'get': 'list', 'post': 'create'} for method, action in actions.items(): # method=get action=list # method=post action=create # 视图类对象中反射:list 字符串--》返回了 list方法 # handler就是list方法 handler = getattr(self, action) # 把handler:list方法 通过反射--》放到了视图类的对象中、 # method:get # 视图类的对象中有个get--》本质是list setattr(self, method, handler) return self.dispatch(request, *args, **kwargs) # APIView的dispatch return csrf_exempt(view)
(4)viewsets下常用的类
-
ViewSetMixin
: 视图类继承它,路由写法变了,一定要放在视图类前面 -
ViewSet
: APIView+ViewSetMixin -
GenericViewSet
: GenericAPIView+ViewSetMixin -
ReadOnlyModelViewSet
: 视图类继承它,只有两个接口 -
ModelViewSet
: 视图类继承它,五个接口都有了- 继承五个视图扩展类加GenericViewSet
(5)总结
"""
from rest_framework.views import APIView
最初写接口的方法
""""""
from rest_framework.generics import GenericAPIView
继承了APIView 要写两个方法
queryset =
serializer_class =
还给了三个重要的查数据方法
get_queryset()
get_serializer()
get_object()
""""""
mixin 五个视图扩展类 要基于GenericAPIView一起使用 无继承,但是封装好了方法供我们使用 super().xxx(request) xxx:对应
from rest_framework.mixins import CreateModelMixin,UpdateModelMixin,RetrieveModelMixin,DestroyModelMixin,ListModelMixin
class BookView(GenericAPIView,ListModelMixin):
def get(self,request):
return super().list(request)
""""""
generic 九个视图子类 继承了各自的扩展方法,又继承了GenericAPIView,其实就是进一步封装了方法
from rest_framework.generics import CreateAPIView,ListAPIView,RetrieveAPIView,DestroyAPIView,UpdateAPIView
from rest_framework.generics import ListCreateAPIView,RetrieveDestroyAPIView,RetrieveUpdateDestroyAPIView,RetrieveUpdateAPIView
class BookView(ListAPIView):
queryset =
serializer_class =
""""""
导入的类都在viewsets中
视图集 最核心类是ViewSetMixin,是基类,一旦继承了这个类,就要重写url内容,必须传参数action
其实要使用ViewSetMixin这个类的话,就必须要搭配另外一个类一起使用
from rest_framework.viewsets import ViewSetMixinViewSet 是 APIView+ViewSetMixin组合而成 最初写接口方式+重写url
from rest_framework.viewsets import ViewSetfrom rest_framework.viewsets import GenericViewSet
GenericViewSet 是 GenericView+ViewSetMixin组合而成 可以少写代码,比如:
class BookView(GenericViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
def get_all(self,request):
obj = self.get_queryset()
serializer = self.get_serializer(instance=obj,many=True)
return Response({'code':200,"msg":'查询所有数据成功','result':serializer.data})ModelViewSet 继承了五个扩展类,又继承了GenericViewSet,集大成为一体,非常强大
因为他封装了mixin中的五个扩展方法,又能重写路由,不用我们自己写五个方法from rest_framework.viewsets import ModelViewSet
urls.py:
router = SimpleRouter()
router.register('book', BookView, 'book')
urlpatterns = []
urlpatterns += router.urlsviews.py:
class BookView(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
"""
(15)路由
-
继承ViewSetMixin,路由写法就变了
-
还可以自动生成路由
views.py
class BookView(ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer
urls.py
from rest_framework.routers import SimpleRouter router=SimpleRouter() # prefix : 前缀 # viewset : 视图类 # basename : 别名 router.register('book',BookView,'book') urlpatterns = [] urlpatterns += router.urls
**第二种写法 **可以添加前缀
from django.urls import path,include from app01.views import User from rest_framework.routers import SimpleRouter router = SimpleRouter() router.register('users',User,'users') urlpatterns = [ path('api/v1/',include(router.urls)) ] # http://127.0.0.1:8000/app01/api/v1/users/...
(1)action装饰器
from rest_framework.decorators import action
from django.shortcuts import render from rest_framework.viewsets import GenericViewSet from .serializer import LoginSerializer, PassWordSerializer, RegisterSerializer from rest_framework.decorators import action from rest_framework.response import Response from .models import Users class User(GenericViewSet): queryset = Users.objects.all() serializer_class = LoginSerializer lookup_field = 'phone' def get_serializer_class(self): if self.action == 'edit_password': return PassWordSerializer if self.action == 'register': return RegisterSerializer else: return self.serializer_class @action(methods=['POST'], detail=False) def login(self, request): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): return Response({'code': 200, 'msg': '登录成功!'}) else: return Response({'code': 404, 'msg': serializer.errors}) @action(methods=['POST'], detail=False) def register(self, request): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): serializer.save() return Response({'code': 200, 'msg': '注册成功!'}) else: return Response({'code': 404, 'msg': serializer.errors}) @action(methods=['POST'], detail=False) def edit_password(self, request): mobile = request.data.get('mobile') obj = Users.objects.filter(mobile=mobile).first() if not obj: return Response({'code': 404, 'msg': '手机号不存在!'}) serializer = self.get_serializer(instance=obj, data=request.data) if serializer.is_valid(): serializer.save() return Response({'code': 200, 'msg': '修改成功!'}) else: return Response({'code': 404, 'msg': serializer.errors})
最主要的是self.action方法
这个方法会返回当前请求路由中映射的方法名
比如我现在请求http://127.0.0.1:8000/app01/users/register/
这个地址,self.action=register
def get_serializer_class(self): if self.action == 'edit_password': return PassWordSerializer if self.action == 'register': return RegisterSerializer else: return self.serializer_class
装饰器有四个属性
methods
: 定义了这个自定义动作所允许的 HTTP 请求方法列表。比如,methods=['POST', 'GET']
表示这个自定义动作允许 POST 和 GET 请求。默认为['GET']
。
detail
: 一个布尔值,指示这个自定义动作是针对单个对象还是整个集合。如果设置为True
,则这个自定义动作将在单个对象上执行,如果设置为False
,则将在整个集合上执行。默认为True
,表示针对单个对象。
传参数的情况:
views.py
from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response class UserViewSet(viewsets.ViewSet): # ...其他代码... @action(methods=['POST'], detail=True) def deactivate(self, request, pk=None): user = self.get_object() user.is_active = False user.save() return Response({'status': 'User deactivated'})
urls.py
from django.urls import path from .views import UserViewSet urlpatterns = [ path('users/<int:pk>/deactivate/', UserViewSet.as_view({'post': 'deactivate'})), ]
url_path
: 用于定义自定义动作的 URL 路径。可以将其设置为自定义的路径,以覆盖默认的 URL 路径。
url_name
: 用于定义自定义动作的 URL 名称。默认情况下,URL 名称与方法名称相同,但你可以通过这个参数自定义名称。
(16)三大认证
(1)认证类
其实就是所谓的登录认证,用户登录成功,就签发一个token,以后需要登录才能访问的接口,必须携带当时签发的token过来,后端验证通过,继续后续操作。
使用步骤
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
1 写个类,继承BaseAuthentication 2 重写 authenticate 方法 3 在 authenticate 中完成用户登录的校验 -用户携带token --> 能查到,就是登录了 -用户没带,查不到,就是没登录 4 如果校验通过,返回当前登录用户和token 5 如果没校验通过,抛异常AuthenticationFailed
authentication.py 认证类
# -*- coding: utf-8 -*- # author : heart # blog_url : https://www.cnblogs.com/ssrheart/ # time : 2024/4/16 import uuid from rest_framework.authentication import BaseAuthentication from .models import Users, UserToken from rest_framework.response import Response from rest_framework.exceptions import AuthenticationFailed class LoginAuth(BaseAuthentication): def authenticate(self, request): token = request.META.get('HTTP_TOKEN') res = UserToken.objects.filter(token=token).first() if res: user = res.user return user, token # 这个uesr存入request.user中 else: raise AuthenticationFailed('请先登录!')
使用方式
# 1 局部使用 class BookView(GenericViewSet): # 写个认证类,在视图类上配置即可 authentication_classes = [LoginAuth] # 2 全局使用 settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'app01.authentication.LoginAuth' ], } # 3 局部禁用 class UserView(ViewSet): authentication_classes = []
(2)权限类
其实就是所谓的权限认证,用户注册完成之后,会有一个权限的字段,在权限类中可以定义只有某个权限的才可以访问数据。
from rest_framework.permissions import BasePermission
权限类的编写 -1 写个类,继承BasePermission -2 重写 has_permission 方法 -3 在方法内部进行权限校验 -有权限返回True -没权限返回False -4 定制返回提示:self.message
permissions.py
from rest_framework.permissions import BasePermission class UserPermission(BasePermission): def has_permission(self, request, view): # 判断权限,如果有权限,返回True,如果没有,返回False # 认证完之后,会把返回的那个user存入request.user中,直接可以取出对象 if request.user.user_type == 3: return True else: user_type = request.user.get_user_type_display() # 这地方可以自定制返回的信息 self.message=f'您是:{user_type},您没有权限访问' return False
使用方式
# 1 局部使用 class BookView(GenericViewSet): # 写个权限类,在视图类上配置即可 permission_classes = [UserPermission] # 2 全局使用 settings.py REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'app01.permissions.UserPermission' ], } # 3 局部禁用 class BookView(GenericViewSet): permission_classes = []
(3)频率类
其实就是限制访问的频率
from rest_framework.throttling import BaseThrottle,SimpleRateThrottle
频率类的编写 -1 写个类,继承SimpleRateThrottle -2 重写 get_cache_key ,返回唯一标识,用来限制 -3 在类中写类属性:rate = '3/m' 做限制
throttling.py
from rest_framework.throttling import BaseThrottle, SimpleRateThrottle class CommonThrottle(SimpleRateThrottle): rate = '3/m' # 一分钟三次 def get_cache_key(self, request, view): # 返回什么,就会以什么做限制 return request.META.get('REMOTE_ADDR')
使用方式
# 局部使用 class PublishView(GenericViewSet): throttle_classes = [CommonThrottle] # 全局使用 REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': [ 'app01.throttling.CommonThrottle' ], } # 局部禁用 class PublishView(GenericViewSet): throttle_classes = []
(17)排序、过滤、分页
(1)排序
-
必须是查询所有接口,restful规范中:地址栏中带过滤条件
-
如果要定制返回格式,需要在重写方法内加入self.filter_queryset()把qs对象包起来,排序才会生效。
http://127.0.0.1:8008/app01/api/v1/books/?ordering=price,-name
from rest_framework.filters import OrderingFilter
filter_backends = [OrderingFilter] # 过滤 ordering_fields = ['price','name'] # 按哪个字段过滤 可以有多个字段
class BookView(GenericViewSet): queryset = Book.objects.all() serializer_class = BookSerializer filter_backends = [OrderingFilter] # 过滤 ordering_fields = ['book_price'] # 按哪个字段过滤 def list(self, request): obj = self.filter_queryset(self.get_queryset()) serializer = BookSerializer(instance=obj,many=True) return Response({'code': 200, 'msg': '查询所有数据成功', 'data': serializer.data})
(2)过滤
- 三种过滤
- drf内置的
- 第三方 django-filter
- 自定义 比较复杂的用它
过滤和排序不冲突
from rest_framework.filters import SearchFilter filter_backends = [SearchFilter] # 过滤 search_fields=['name','publish'] # 按字段模糊过滤 也可以多个字段
http://127.0.0.1:8000/books/bookview/?search=ssr http://127.0.0.1:8000/books/bookview/?search=ssr&ordering=-book_price # 可以两个一起过滤,不冲突
第三方过滤 按名字精准匹配
安装第三方模块
pip install django-filter
from django_filters.rest_framework import DjangoFilterBackend
# 查询价格为222的图书 http://127.0.0.1:8000/books/bookview/?book_price=222
filter_backends = [DjangoFilterBackend] # 过滤 filterset_fields=['book_price','book_name']
自定义过滤类
# 查询价格为222 或 名字中包含ssr的数据
from rest_framework.filters import BaseFilterBackend
使用的步骤 -1 定义一个类 继承 BaseFilterBackend -2 在类中重写 filter_queryset -3 返回qs对象
要取字段的话,使用request.query_params.get()
from rest_framework.filters import BaseFilterBackend from django.db.models import Q class CommonFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): # 完成过滤,返回qs对象 book_price = request.query_params.get('book_price', None) book_name = request.query_params.get('book_name', None) # queryset 其实就是 Book.objects.all() queryset =queryset.filter(Q(book_price=book_price) | Q(book_name__contains=book_name)) return queryset
filter_backends = [CommonFilter] # 过滤
(3)分页
PageNumberPagination
pagination.py
page_size
: 每页显示多少条
page_query_param
: 分页关键字
page_size_query_param
: 每页显示条数
max_page_size
: 最多显示
from rest_framework.pagination import LimitOffsetPagination,PageNumberPagination class CommonPagination(PageNumberPagination): page_size = 2 # 每页显示多少条 page_query_param = 'page' # http://127.0.0.1:8000/book/bookview/?page=2 分页 page_size_query_param = 'size' # http://127.0.0.1:8000/book/bookview/?page=1&size=3 每页显示条数 max_page_size = 10 # 不管size输入多少,最多显示10条
pagination_class = CommonPagination
LimitOffsetPagination
class CommonLimitOffsetPagination(LimitOffsetPagination): default_limit = 2 # 每页显示两条 # http://127.0.0.1:8008/app01/api/v1/books1/?limit=4 一页显示四条 limit_query_param = 'limit' # 控制每页显示多少条 # http://127.0.0.1:8008/app01/api/v1/books1/?offset=3 从第3条开始,取两条 # http://127.0.0.1:8008/app01/api/v1/books1/?offset=3&limit=1 从第3条开始,取1条 offset_query_param = 'offset' # 偏移量 max_limit = 10 # 最大每页取10条
pagination_class = LimitOffsetPagination
CursorPagination
# 游标分页-->必须要排序--》只能取上一页和下一页,不能直接跳转到某一页 效率高, 大数据量 app端使用 class CommonCursorPagination(CursorPagination): cursor_query_param = 'cursor' # 查询条件 http://127.0.0.1:8008/app01/api/v1/books1/?cursor=asfasf page_size = 2 ordering = 'id'
pagination_class = CursorPagination
(18)全局异常处理
from rest_framework.exceptions import ValidationError,AuthenticationFailed,APIException
from rest_framework.views import exception_handler
# 1 回顾:APIView的dispatch的时候--》三大认证,视图类的方法中--》出了异常--》被异常捕获--》都会执行一个函数: -只要出了异常,都会执行response = self.handle_exception(exc) -handle_exception 源码分析 def handle_exception(self, exc): # 拿到一个函数内存地址--》配置文件中配置了 # self.settings.EXCEPTION_HANDLER # 拿到这个函数:rest_framework.views.exception_handler exception_handler = self.get_exception_handler() # 执行这个函数,传入俩参数 response = exception_handler(exc, context) # drf的 Response的对象 return response -rest_framework.views.exception_handler 逻辑分析 -判断异常是不是 drf内 exceptions.APIException的异常 -如果是--》组装了Response返回了 -如果不是--》返回了None -总结:默认drf只处理了自己的异常,django的异常,它没处理,直接前端能看到
全局异常处理使用
使用方法 1 写个函数 2 在函数中处理异常,统一返回格式 3 在配置文件中配置:只要出了异常,执行我们自己的 REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'app01.exceptions.common_exception_handler', }
异常函数
# -*- coding: utf-8 -*- # author : heart # blog_url : https://www.cnblogs.com/ssrheart/ # time : 2024/4/18 from rest_framework.views import exception_handler from rest_framework.response import Response # 自定义异常类 class PasswordException(Exception): def __init__(self, msg): self.msg = msg import time # 记录错误日志:时间,请求方式,请求的地址,客户端ip,用户id def common_exception_handler(exc, context): """ :param exc: 错误的原因 division by zero :param context: 触发错误的各种参数 { 'view': <app01.views.testExc object at 0x000002058BF2A740>, 'args': (), 'kwargs': {}, 'request': <rest_framework.request.Request: GET '/app01/testexc/'> } """ request = context.get('request') create_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) method = request.method addr = request.get_full_path() ip = request.META.get('REMOTE_ADDR') id = request.user view = context.get('view') log = [create_time, method, addr, ip, id,view] with open('log.log', 'a', encoding='utf-8') as f: f.write('\n' + str(log)) response = exception_handler(exc, context) if response: if isinstance(response.data, dict): error = response.data.get('detail', '系统错误,请联系heartt') return Response({"code": 991, 'msg': f"{error}"}) elif isinstance(response.data, list): error = response.data[0] else: error = '系统错误,请联系heartt' return Response({"code": 992, 'msg': f"{error}"}) else: if isinstance(exc, ZeroDivisionError): response = Response({"code": 993, 'msg': f"{str(exc)}"}) elif isinstance(exc, PasswordException): response = Response({"code": 994, 'msg': f"密码错误"}) elif isinstance(exc, Exception): response = Response({"code": 995, 'msg': f"{str(exc)}"}) else: response = Response({"code": 999, 'msg': f"{str(exc)}"}) return response
自定义异常类
# 自定义异常类 class PasswordException(Exception): def __init__(self, msg): self.msg = msg # 视图函数 class testExc(APIView): def get(self, request): raise PasswordException('asdzxvqweqr')
(19)coreapi
自动生成接口文档
pip install coreapi
from rest_framework.documentation import include_docs_urls urlpatterns = [ path('docs/', include_docs_urls(title='DRFTEST')) ] # 3 配置文件配置 REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', } # 4 正常写视图类 -在方法中加的注释,会在接口文档中体现 -比如在models表中写help_text -在序列化类中写的 -required
(20)jwt
JWT(json web token)是一种前后端登录认证的方案,区别于之前的cookie和session。
它有签发阶段,登录成功后签发。
它有认证阶段,需要登录后才能访问的接口,通过认证token之后,才能继续操作。
全程没有在数据库中进行存储,性能高,安全。
- JWT开发重点
- 签发token
- 校验token
签发的token是典型的三段式,用.分割,每一段使用base64编码,字符串长度一定是4的倍数,如果不是,用=补齐,但是=只是占位符
eyJuYW1lIjogImhlYXJ0IiwgImFnZSI6IDE4fQ==.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
- 第一部分我们称它为头部(header),一般放公司信息,加密方式,jwt,一般都是固定的
- 第二部分我们称其为载荷(payload),一般放登录用户信息,用户名,用户id,过期时间,签发时间,是否是超级用户...
- 第三部分是签证(signature),是加密后的串二进制数据,是前两段数据拼接之后加密而成
- 签发阶段:通过头和荷载 使用 某种加密方式[HS256,md5,sha1]得到
- 校验阶段:拿到token,取出第一和第二部分,通过之前同样的加密方式得到新的第三段签名,用新签名和老签名比较,如果一样说明没被篡改过,就信任,直接去拿数据就可以了
base64
它不是加密方案,而是编码方案,没有加密
- 它可以在网络中传输,字符串编码成base64
- 图片使用base64编码,传给前端
- jwt中使用
import base64 import json userinfo = {'name': 'heart', 'age': 18} userinfo_str = json.dumps(userinfo) # base64 编码 res = base64.b64encode(userinfo_str.encode(encoding='utf-8')) print(res) # b'eyJuYW1lIjogImhlYXJ0IiwgImFnZSI6IDE4fQ==' # base64 解码 res1 = base64.b64decode(res) res2 = json.loads(res1) print(res1) # b'{"name": "heart", "age": 18}' print(res2) # {'name': 'heart', 'age': 18} # 图片 s = 'data:image/jpg;base64,iVBORw0KGgoAAAANSUh...' ss = s.split(',')[-1] sss = base64.b64decode(ss) with open('12306.png','wb')as f: f.write(sss)
(1)simple-jwt使用
pip install djangorestframework-simplejwt
快速体验
登录签发:默认使用auth的user表,创建个用户就能登录了
认证:需要加两句话,而且必须是使用auth的user表使用
from rest_framework_simplejwt.views import token_obtain_pair urlpatterns = [ path('login/',token_obtain_pair) ]
给这个地址发送post请求登录http://127.0.0.1:8000/app01/login/,就能获取到JWT签发下来的token了
然后需要拿着这个token中的access对应的内容,带到请求头中访问需要登录的接口,就可以进行接下来的操作了。
接口中需要定义认证类和权限类,这是jwt中必须要携带的两个参数
from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework.permissions import IsAuthenticated authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated]
views.py
from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework.permissions import IsAuthenticated from .serializer import UserTokenSerializer class UserTokenView(GenericViewSet, CreateModelMixin): queryset = UserToken.objects.all() serializer_class = UserTokenSerializer authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated]
(2)simple-jwt配置文件
# JWT配置 SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Access Token的有效期 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token的有效期 # 对于大部分情况,设置以上两项就可以了,以下为默认配置项目,可根据需要进行调整 # 是否自动刷新Refresh Token 'ROTATE_REFRESH_TOKENS': False, # 刷新Refresh Token时是否将旧Token加入黑名单,如果设置为False,则旧的刷新令牌仍然可以用于获取新的访问令牌。需要将'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中 'BLACKLIST_AFTER_ROTATION': False, 'ALGORITHM': 'HS256', # 加密算法 'SIGNING_KEY': settings.SECRET_KEY, # 签名密匙,这里使用Django的SECRET_KEY # 如为True,则在每次使用访问令牌进行身份验证时,更新用户最后登录时间 "UPDATE_LAST_LOGIN": False, # 用于验证JWT签名的密钥返回的内容。可以是字符串形式的密钥,也可以是一个字典。 "VERIFYING_KEY": "", "AUDIENCE": None,# JWT中的"Audience"声明,用于指定该JWT的预期接收者。 "ISSUER": None, # JWT中的"Issuer"声明,用于指定该JWT的发行者。 "JSON_ENCODER": None, # 用于序列化JWT负载的JSON编码器。默认为Django的JSON编码器。 "JWK_URL": None, # 包含公钥的URL,用于验证JWT签名。 "LEEWAY": 0, # 允许的时钟偏差量,以秒为单位。用于在验证JWT的过期时间和生效时间时考虑时钟偏差。 # 用于指定JWT在HTTP请求头中使用的身份验证方案。默认为"Bearer" "AUTH_HEADER_TYPES": ("Bearer",), # 包含JWT的HTTP请求头的名称。默认为"HTTP_AUTHORIZATION" "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", # 用户模型中用作用户ID的字段。默认为"id"。 "USER_ID_FIELD": "id", # JWT负载中包含用户ID的声明。默认为"user_id"。 "USER_ID_CLAIM": "user_id", # 用于指定用户身份验证规则的函数或方法。默认使用Django的默认身份验证方法进行身份验证。 "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", # 用于指定可以使用的令牌类。默认为"rest_framework_simplejwt.tokens.AccessToken"。 "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), # JWT负载中包含令牌类型的声明。默认为"token_type"。 "TOKEN_TYPE_CLAIM": "token_type", # 用于指定可以使用的用户模型类。默认为"rest_framework_simplejwt.models.TokenUser"。 "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", # JWT负载中包含JWT ID的声明。默认为"jti"。 "JTI_CLAIM": "jti", # 在使用滑动令牌时,JWT负载中包含刷新令牌过期时间的声明。默认为"refresh_exp"。 "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", # 滑动令牌的生命周期。默认为5分钟。 "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), # 滑动令牌可以用于刷新的时间段。默认为1天。 "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), # 用于生成access和refresh的序列化器。 "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer", # 用于刷新访问令牌的序列化器。默认 "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer", # 用于验证令牌的序列化器。 "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer", # 用于列出或撤销已失效JWT的序列化器。 "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer", # 用于生成滑动令牌的序列化器。 "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer", # 用于刷新滑动令牌的序列化器。 "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", }
项目中配置
from datetime import timedelta SIMPLE_JWT ={ 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Access Token的有效期 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token的有效期 "AUTH_HEADER_TYPES": ("HEART",), # 自定义请求头前面携带 (Bearer) }
(3)定制jwt自带的login返回格式
流程
1 编写(重写)序列化类,给登录接口用 simple-jwt 写的登录接口默认使用: rest_framework_simplejwt.serializers.TokenObtainPairSerializer 2 自己写个序列化类,定制序列化的字段 -重写全局钩子 用来校验用户名和密码 -校验通过:把校验通过的user放到self.user中 -定制返回格式返回 3 在settings中配置生效
自己写序列化类
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer class CommonToken(TokenObtainPairSerializer): # 重写全局钩子 def validate(self, attrs): # 校验还用原来的--》校验用户名和密码 # 校验通过:把校验通过的user放到self.user 中 # 签发token dic = super().validate(attrs) data = { "code": 100, "msg": '登录成功!', "username": self.user.username, "refresh": dic.get('refresh'), "access": dic.get('access') } return data
路由配置
from rest_framework_simplejwt.views import token_obtain_pair urlpatterns = [ path('login/',token_obtain_pair) ]
settings.py
SIMPLE_JWT ={ 'TOKEN_OBTAIN_SERIALIZER':'app01.serializer.CommonToken' }
(4)定制jwt自带的login返回的payload格式
流程
1 编写(重写)序列化类,给登录接口用 simple-jwt 写的登录接口默认使用: rest_framework_simplejwt.serializers.TokenObtainPairSerializer 2 自己写个序列化类,定制序列化的字段 -在调用get_token后往里面添加值 -重写全局钩子 用来校验用户名和密码 -校验通过:把校验通过的user放到self.user中 -定制返回格式返回 3 在settings中配置生效
自己重写序列化类
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer class CommonTokenObtainSerializer(TokenObtainPairSerializer): @classmethod def get_token(cls, user): # super()--->代指父类对象--》 # 对象调用类的绑定方法--->会自动把对象的类传入 token = super().get_token(user) # 这个地方在拿token token['name'] = user.username return token def validate(self, attrs): super().validate(attrs) token = self.get_token(self.user) data = {'code': 100, 'msg': '登录成功', 'username': self.user.username, 'refresh': str(token), 'access': str(token.access_token) } return data
路由配置
from rest_framework_simplejwt.views import token_obtain_pair urlpatterns = [ path('login/',token_obtain_pair) ]
settings.py
SIMPLE_JWT = { 'TOKEN_OBTAIN_SERIALIZER': 'app01.serializer.CommonTokenObtainSerializer' }
(5)多方式登录
# 1 使用auth的user表 只能传用户名 ,密码校验 # 2 项目中:手机号/用户名/邮箱 + 密码 也可以登录成功 但是simple-jwt就不行了 # 3 自己定制登陆接口--》使用auth的user表 -签发自己签发 -认证继续用 simple-jwt的认证即可 # 4 编写一个多方式登陆接口 - 扩写auth的user表---》加入mobile字段 - 编写登陆接口
这个地方可以使用 serializer.context
进行视图函数和序列化之间的数据传输
serializer
签发token使用 RefreshToken.for_user(对象)
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework import serializers from .models import UserInfo import re from rest_framework.exceptions import ValidationError from rest_framework_simplejwt.tokens import RefreshToken class UserSerializer(serializers.Serializer): username = serializers.CharField() # 可能是 用户名 手机号 邮箱 password = serializers.CharField() def _get_user(self, attrs): username = attrs.get('username') password = attrs.get('password') if re.match(r'^1[3-9][0-9]{9}$', username): user = UserInfo.objects.filter(mobile=username).first() elif re.match('^.+@.+$', username): user = UserInfo.objects.filter(email=username).first() else: user = UserInfo.objects.filter(username=username).first() if user and user.check_password(password): return user else: raise ValidationError('用户名或密码错误') def validate(self, attrs): user = self._get_user(attrs) token = RefreshToken.for_user(user) self.context['refresh'] = str(token) self.context['access'] = str(token.access_token) return attrs
views
class UserView(GenericViewSet): serializer_class = UserSerializer @action(methods=['POST'], detail=False) def login(self, request): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): refresh = serializer.context.get('refresh') access = serializer.context.get('access') return Response({'code': 100, 'msg': '登录成功', 'refresh': refresh, 'access': access}) else: return Response({'code': 101, 'msg': serializer.errors})
(6)自定义用户表,手动签发和认证
手动签发
序列化类 最重要的是签发的部分 RefreshToken.for_user(user)
from rest_framework import serializers from .models import User from rest_framework.exceptions import ValidationError from rest_framework_simplejwt.tokens import RefreshToken class UserSerializer(serializers.Serializer): username = serializers.CharField() password = serializers.CharField() def get_user(self, attrs): username = attrs.get('username') password = attrs.get('password') user = User.objects.filter(username=username, password=password).first() if user: return user else: raise ValidationError('用户名或密码错误') def validate(self, attrs): user = self.get_user(attrs) token = RefreshToken.for_user(user) self.context['refresh'] = str(token) self.context['access_token'] = str(token.access_token) return attrs
views.py
class UserRZView(GenericViewSet): serializer_class = UserSerializer @action(methods=['POST'], detail=False) def login(self, request): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): refresh = serializer.context.get('refresh') access = serializer.context.get('access_token') return Response({'code': 200, 'msg': '登陆成功', 'refresh': refresh, 'access': access}) else: return Response({'code': 201, 'msg': serializer.errors})
认证 最重要的是验证部分,当然要继承JWTAuthentication
这个类之后调用类方法: self.get_validated_token(token)
返回的是可以信任的payload,可以直接用字典取值
认证类
from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework.exceptions import AuthenticationFailed from .models import User class JWTAuth(JWTAuthentication): def authenticate(self, request): token = request.META.get('HTTP_AUTHORIZATION') if token: # 校验token validated_token 返回的就是可以信任的payload validated_token = self.get_validated_token(token) user_id = validated_token['user_id'] user = User.objects.filter(pk=user_id).first() return user, token else: raise AuthenticationFailed('请先登录!')
使用
from .permission import JWTAuth from rest_framework.mixins import ListModelMixin class PublishView(GenericViewSet,ListModelMixin): authentication_classes = [JWTAuth]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通