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]