预备知识:
-- 遵循rest风格实现的前后端交互都叫RESTful架构
-- URI:统一资源标识符,相当于身份证号
-- URL:统一资源定位符,相当于姓名
RESTful规范:
一、核心思想
1、面向资源编程,url中尽量用名词而不是动词
2、根据HTTP请求方式的不同对资源进行不同操作。
二、在url中体现的规范
1、体现版本
2、体现是否是API
3、有过滤条件
4、尽量用https
三、在返回值中的规范
1、携带状态码
2、返回值:get返回查看的所有或单条数据;post返回新增的这条数据;put / patch 返回更新的这条数据;delete 返回值空
3、携带错误信息
4、携带超链接
RestFramework
使用:
1、pip install djangorestframework
2、在settings的app中注册rest_framework
一、序列化
from SerDemo import models
from SerDemo.serializers import BookSerializer
from rest_framework.response import Response
from rest_framework.views import APIView
# Create your views here.
class BookView(APIView):
def get(self, request, book_id=0):
if book_id == 0:
books_query = models.Books.objects.all()
else:
books_query = models.Books.objects.filter(id=book_id)
ret = BookSerializer(books_query, many=True)
return Response(ret.data) # 序列化后的数据保存在.data里
def post(self, request):
book_obj = request.data
ser_obj = BookSerializer(data=book_obj)
if ser_obj.is_valid():
ser_obj.save()
return Response(ser_obj.validated_data)
return Response(ser_obj.errors)
def put(self, request, book_id):
book_obj = models.Books.objects.filter(id=book_id).first()
ser_obj = BookSerializer(instance=book_obj, data=request.data, partial=True) # 表示部分校验
if ser_obj.is_valid():
ser_obj.save()
return Response(ser_obj.validated_data)
return Response(ser_obj.errors)
views.py
from SerDemo import models
from rest_framework import serializers
class AuthorsSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField(max_length=32)
class PublisherSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField(max_length=32)
class BookSerializer(serializers.Serializer):
id = serializers.IntegerField(required=False) # 表示验证时不需要传
tittle = serializers.CharField(max_length=32)
publisher = PublisherSerializer(read_only=True) # 只在取值时验证
publisher_id = serializers.IntegerField(write_only=True) # 只在传值时验证
authors = AuthorsSerializer(many=True, read_only=True) # many表示需要循环
authors_list = serializers.ListField(write_only=True)
def create(self, validated_data): # 创建数据需要自定义create方法
book_obj = models.Books.objects.create(tittle=validated_data["tittle"], publisher_id=validated_data["publisher_id"])
book_obj.authors.add(*validated_data["authors_list"])
return book_obj
def update(self, instance, validated_data): # 更新数据需要自定义update方法
instance.tittle = validated_data.get("tittle", instance.tittle)
instance.publisher_id = validated_data.get("publisher_id", instance.publisher_id)
if validated_data.get("authors_list"):
instance.authors.set(validated_data["authors_list"])
instance.save() # 注意这一步
return instance
serializers.py
restframework的序列化也提供了钩子函数,用法和forms一样,不过是validate_tittle形式
def validate_tittle(self, value):
# value就是tittle的值 对value处理
if "python" not in value.lower():
raise serializers.ValidationError("标题必须含有python")
return value
def validate(self, attrs):
# attrs 字典有你传过来的所有的字段
print(attrs)
if "python" in attrs["title"].lower() or attrs["post_category"] == 1:
return attrs
else:
raise serializers.ValidationError("分类或标题不合符要求")
class BookSerializer(serializers.ModelSerializer):
publisher_info = serializers.SerializerMethodField(read_only=True) # 会在查看时新增加一组键值
authors_info = serializers.SerializerMethodField(read_only=True)
def get_authors_info(self, obj): # obj就是传入的Book对象
authors_querset = obj.authors.all()
return [{"id": author.id, "name": author.name} for author in authors_querset]
def get_publisher_info(self, obj):
publisher_obj = obj.publisher
return {"id": publisher_obj.id, "title": publisher_obj.title}
class Meta:
model = models.Books
fields = "__all__"
# exclude=["id"]
# depth = 1 # 会让你这些所有的外键关系变成read_only = True,并且会显示所有字段,一般不用,会自定义
extra_kwargs = {"publisher": {"write_only": True}, "authors": {"write_only": True}}
ModelSerializers
二、视图
url(r'^book$', BookModelView.as_view({"get": "list", "post": "create"})),
url(r'^book/(?P<pk>\d+)', BookModelView.as_view({"get": "retrieve", "put": "update", "delete": "destroy"})),
from rest_framework import viewsets
class BookModelView(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
ModelViewSets
GenericViewSet 继承了ViewSetMixin,里面重写了as_view方法,让as_view可以接受参数,并且将请求方式与对应的方法对应起来,所以继承了ModelViewSet方法就能给as_view传参。
GenericViewSet
一共可以说有三次封装,第一次封装将获取queryset对象以及获取序列化器的两个方法封装到GenericAPIView
class GenericAPIView(views.APIView):
queryset = None
serializer_class = None
def get_queryset(self):
queryset = self.queryset
if isinstance(queryset, QuerySet):
# Ensure queryset is re-evaluated on each request.
queryset = queryset.all()
return queryset
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
def get_serializer_class(self):
return self.serializer_class
GenericAPIView
第二次封装,将各个视图方法封装成各个类ListModelMixin,CreateModelMixin,RetrieveModelMixin,UpdateModelMixin,DestroyModelMin
地三次封装,重写as_view方法,让as_view可以接受参数,在self.dispatch之前,将self.get = self.list......
三、路由组件
rest_framework提供了自动生成传参路由的功能
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'^books')
urlpatterns += router.urls
这样会自动生成多个路由,包括books/(\d+),且都as_view中都带有参数,对应视图必须可以接受参数,但是一般不使用,因为生成的路由多也是一种浪费,也会有风险。
四、版本控制
在APIView的dispatch方法中调用了initial方法,其中包含了版本控制的代码:
# Determine the API version, if versioning is in use.
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme # 版本和使用的类
如果没有version_class类,将把None赋值给上面两个属性,默认的version_class类是api.settings.DEFAULT_VERSIONING_CLASS,默认是None。
restframework给我们提供了一些类,用来做版本控制,可以通过url匹配、params等方法获取版本信息:
# coding: utf-8
from __future__ import unicode_literals
import re
from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions
from rest_framework.compat import unicode_http_header
from rest_framework.reverse import _reverse
from rest_framework.settings import api_settings
from rest_framework.templatetags.rest_framework import replace_query_param
from rest_framework.utils.mediatypes import _MediaType
class BaseVersioning(object):
default_version = api_settings.DEFAULT_VERSION
allowed_versions = api_settings.ALLOWED_VERSIONS
version_param = api_settings.VERSION_PARAM
def determine_version(self, request, *args, **kwargs):
msg = '{cls}.determine_version() must be implemented.'
raise NotImplementedError(msg.format(
cls=self.__class__.__name__
))
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
return _reverse(viewname, args, kwargs, request, format, **extra)
def is_allowed_version(self, version):
if not self.allowed_versions:
return True
return ((version is not None and version == self.default_version) or
(version in self.allowed_versions))
class AcceptHeaderVersioning(BaseVersioning):
"""
GET /something/ HTTP/1.1
Host: example.com
Accept: application/json; version=1.0
"""
invalid_version_message = _('Invalid version in "Accept" header.')
def determine_version(self, request, *args, **kwargs):
media_type = _MediaType(request.accepted_media_type)
version = media_type.params.get(self.version_param, self.default_version)
version = unicode_http_header(version)
if not self.is_allowed_version(version):
raise exceptions.NotAcceptable(self.invalid_version_message)
return version
# We don't need to implement `reverse`, as the versioning is based
# on the `Accept` header, not on the request URL.
class URLPathVersioning(BaseVersioning):
"""
To the client this is the same style as `NamespaceVersioning`.
The difference is in the backend - this implementation uses
Django's URL keyword arguments to determine the version.
An example URL conf for two views that accept two different versions.
urlpatterns = [
url(r'^(?P<version>[v1|v2]+)/users/$', users_list, name='users-list'),
url(r'^(?P<version>[v1|v2]+)/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
]
GET /1.0/something/ HTTP/1.1
Host: example.com
Accept: application/json
"""
invalid_version_message = _('Invalid version in URL path.')
def determine_version(self, request, *args, **kwargs):
version = kwargs.get(self.version_param, self.default_version)
if version is None:
version = self.default_version
if not self.is_allowed_version(version):
raise exceptions.NotFound(self.invalid_version_message)
return version
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request.version is not None:
kwargs = {} if (kwargs is None) else kwargs
kwargs[self.version_param] = request.version
return super(URLPathVersioning, self).reverse(
viewname, args, kwargs, request, format, **extra
)
class NamespaceVersioning(BaseVersioning):
"""
To the client this is the same style as `URLPathVersioning`.
The difference is in the backend - this implementation uses
Django's URL namespaces to determine the version.
An example URL conf that is namespaced into two separate versions
# users/urls.py
urlpatterns = [
url(r'^/users/$', users_list, name='users-list'),
url(r'^/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
]
# urls.py
urlpatterns = [
url(r'^v1/', include('users.urls', namespace='v1')),
url(r'^v2/', include('users.urls', namespace='v2'))
]
GET /1.0/something/ HTTP/1.1
Host: example.com
Accept: application/json
"""
invalid_version_message = _('Invalid version in URL path. Does not match any version namespace.')
def determine_version(self, request, *args, **kwargs):
resolver_match = getattr(request, 'resolver_match', None)
if resolver_match is None or not resolver_match.namespace:
return self.default_version
# Allow for possibly nested namespaces.
possible_versions = resolver_match.namespace.split(':')
for version in possible_versions:
if self.is_allowed_version(version):
return version
raise exceptions.NotFound(self.invalid_version_message)
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request.version is not None:
viewname = self.get_versioned_viewname(viewname, request)
return super(NamespaceVersioning, self).reverse(
viewname, args, kwargs, request, format, **extra
)
def get_versioned_viewname(self, viewname, request):
return request.version + ':' + viewname
class HostNameVersioning(BaseVersioning):
"""
GET /something/ HTTP/1.1
Host: v1.example.com
Accept: application/json
"""
hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
invalid_version_message = _('Invalid version in hostname.')
def determine_version(self, request, *args, **kwargs):
hostname, separator, port = request.get_host().partition(':')
match = self.hostname_regex.match(hostname)
if not match:
return self.default_version
version = match.group(1)
if not self.is_allowed_version(version):
raise exceptions.NotFound(self.invalid_version_message)
return version
# We don't need to implement `reverse`, as the hostname will already be
# preserved as part of the REST framework `reverse` implementation.
class QueryParameterVersioning(BaseVersioning):
"""
GET /something/?version=0.1 HTTP/1.1
Host: example.com
Accept: application/json
"""
invalid_version_message = _('Invalid version in query parameter.')
def determine_version(self, request, *args, **kwargs):
version = request.query_params.get(self.version_param, self.default_version)
if not self.is_allowed_version(version):
raise exceptions.NotFound(self.invalid_version_message)
return version
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
url = super(QueryParameterVersioning, self).reverse(
viewname, args, kwargs, request, format, **extra
)
if request.version is not None:
return replace_query_param(url, self.version_param, request.version)
return url
rest_framework.versioning
使用方式:
# 在settings中配置要使用的版本控制类
REST_FRAMEWORK = {
# 默认使用的版本控制类
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
# 允许的版本
'ALLOWED_VERSIONS': ['v1', 'v2'],
# 版本使用的参数名称
'VERSION_PARAM': 'version',
# 默认使用的版本
'DEFAULT_VERSION': 'v1',
}
urlpatterns = [
url(r"^versions", MyView.as_view()),
url(r"^(?P<version>[v1|v2]+)/test01", TestView.as_view()),
]
class TestView(APIView):
def get(self, request, *args, **kwargs):
print(request.versioning_scheme)
ret = request.version
if ret == "v1":
return Response("版本v1的信息")
elif ret == "v2":
return Response("版本v2的信息")
else:
return Response("根本就匹配不到这个路由")
五、认证组件
在APIView中有一个认证组件,在initial方法里,版本控制的数据处理后进行认证:
self.perform_authentication(request) # 执行request.user
def perform_authentication(self, request):
request.user
在Request类中:
@property
def user(self):
if not hasattr(self, '_user'):
with wrap_attributeerrors():
self._authenticate()
return self._user
def _authenticate(self):
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise
if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple
return
self._not_authenticated()
_authenticate
self.authoenticators是在实例化Request类时传递进来的,authenticators=self.get_authenticators()
def get_authenticators(self):
return [auth() for auth in self.authentication_classes]
authentication_classes默认是空的,所以需要自己定义,传递的就是每个认证类的实例化对象,对象中需要定义authenticate方法,返回一个元祖,分别赋值给request.user和request.auth。
使用:
这里认证的方式是通过token,在用户表中有一个token字段,每次登陆成功都会重新生成一个uuid,将这个uuid传递给前端,作为当前用户的token,后面每次访问都需要携带这个token。
from rest_framework.exception import AuthenticationFailed
class MyAuth(BaseAuthentication):
def authenticate(self, request):
request_token = request.query_params.get("token", None)
if not request_token:
raise AuthenticationFailed({"code": 1001, "error": "缺少token"})
token_obj = UserInfo.objects.filter(token=request_token).first()
if not token_obj:
raise AuthenticationFailed({"code": 1001, "error": "无效的token"})
return token_obj.username, token_obj
MyAuth类
class TestAuthView(APIView):
authentication_classes = [MyAuth, ]
def get(self, request, *args, **kwargs):
return Response("测试认证")
视图类
如果需要全局配置:
REST_FRAMEWORK = {
# 默认使用的版本控制类
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
# 允许的版本
'ALLOWED_VERSIONS': ['v1', 'v2'],
# 版本使用的参数名称
'VERSION_PARAM': 'version',
# 默认使用的版本
'DEFAULT_VERSION': 'v1',
# 配置全局认证
'DEFAULT_AUTHENTICATION_CLASSES': ["BRQP.utils.MyAuth", ]
}
settings.py
六、权限组件
权限组件的源码和认证组件几乎一样,需要自定义权限校验的类
class MyPermission(BasePermission):
message = "VIP用户才能访问"
def has_permission(self, request, view):
"""
自定义权限只有vip用户能访问,
注意我们初始化时候的顺序是认证在权限前面的,所以只要认证通过~
我们这里就可以通过request.user,拿到我们用户信息
request.auth就能拿到用户对象
"""
if request.user and request.auth.type == 2:
return True
else:
return False
class TestAuthView(APIView):
authentication_classes = [MyAuth, ]
permission_classes = [MyPermission, ]
def get(self, request, *args, **kwargs):
print(request.user)
print(request.auth)
username = request.user
return Response(username)
REST_FRAMEWORK = {
# 默认使用的版本控制类
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
# 允许的版本
'ALLOWED_VERSIONS': ['v1', 'v2'],
# 版本使用的参数名称
'VERSION_PARAM': 'version',
# 默认使用的版本
'DEFAULT_VERSION': 'v1',
# 配置全局认证
# 'DEFAULT_AUTHENTICATION_CLASSES': ["BRQP.utils.MyAuth", ]
# 配置全局权限
"DEFAULT_PERMISSION_CLASSES": ["BROP.utils.MyPermission"]
}
全局使用
七、频率组件
# by gaoxin
from rest_framework.throttling import BaseThrottle
import time
VISIT_RECORD = {}
class MyThrottle(BaseThrottle):
def __init__(self):
self.history = None
def allow_request(self, request, view):
# 实现限流的逻辑
# 以IP限流
# 访问列表 {IP: [time1, time2, time3]}
# 1, 获取请求的IP地址
ip = request.META.get("REMOTE_ADDR")
# 2,判断IP地址是否在访问列表
now = time.time()
if ip not in VISIT_RECORD:
# --1, 不在 需要给访问列表添加key,value
VISIT_RECORD[ip] = [now,]
return True
# --2 在 需要把这个IP的访问记录 把当前时间加入到列表
history = VISIT_RECORD[ip]
history.insert(0, now)
# 3, 确保列表里最新访问时间以及最老的访问时间差 是1分钟
while history and history[0] - history[-1] > 60:
history.pop()
self.history = history
# 4,得到列表长度,判断是否是允许的次数
if len(history) > 3:
return False
else:
return True
def wait(self):
# 返回需要再等多久才能访问
time = 60 - (self.history[0] - self.history[-1])
return time
自定义的权限类
REST_FRAMEWORK = {
# ......
# 频率限制的配置
"DEFAULT_THROTTLE_CLASSES": ["Throttle.throttle.MyThrottle"],
}
}
配置自定义权限类
from rest_framework.throttling import SimpleRateThrottle
class MyVisitThrottle(SimpleRateThrottle):
scope = "WD"
def get_cache_key(self, request, view):
return self.get_ident(request)
使用自带的频率组件
REST_FRAMEWORK = {
# 频率限制的配置
# "DEFAULT_THROTTLE_CLASSES": ["Throttle.throttle.MyVisitThrottle"],
"DEFAULT_THROTTLE_CLASSES": ["Throttle.throttle.MyThrottle"],
"DEFAULT_THROTTLE_RATES":{
'WD':'5/m', #速率配置每分钟不能超过5次访问,WD是scope定义的值,
}
}
配置频率限制
八、分页
九、解析器
content-type告诉对方是什么样的数据类型。
- multipart/form-data 对应文件类型数据
- application/x-www-form-urlencoded 对应表单数据类型
- application/json 对应json数据类型
accept告诉对方我能解析什么样的数据类型。
十、渲染器
rest_framework提供了两种测试接口的渲染器,一种是默认的、有页面样式的模板,一种是纯json数据,但是无法自定义请求头,所以还是需要postman来测试接口。