Django REST framework API开发
REST
介绍
RESTful API 设计
实现API的两种方式
FBV 视图函数
urlpatterns = [url(r'^user/$', views.user),url(r'^user/add/$', views.user_add),url(r'^user/edit/(\d+)/$', views.user_edit),url(r'^user/del/(\d+)/$', views.user_del),
]
传统的视图函数方式,API接口太多,难以维护。
CBV 视图类
urlpatterns = [url(r'user/$', views.UserView.as_view()), # GET, POSTurl(r'user/(\d+)$', views.UserView.as_view()), # PUT, DELETE
]
根据请求方式的不同,执行视图类中对应的方法。同样是实现增删改查,url少一半。这也是面向资源编程的方式,特点是url中都是名词。
CBV相关知识参考:http://blog.csdn.net/ayhan_huang/article/details/78036501#t11
协议
大神说:API与用户的通信协议,总是使用HTTPs协议
域名
http://api.example.com
尽量使用专用的二级域名http://www.example.com/api/
路由分发。如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。
对应前后端分离的项目,可以这样分配:
前端VUE项目使用域名:http://www.example.com
后端API使用域名:http://api.example.com
版本
应该将API的版本号放入URL。
比如:https://www.example.com/api/v1/
v1是版本信息
路径
路径又称”终点”(endpoint),表示API的具体网址。
在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的”集合”(collection),所以API中的名词也应该使用复数。
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/employees
method
- GET :从服务器取出资源(一项或多项)
- POST :在服务器新建一个资源
- PUT :在服务器更新资源(客户端提供改变后的完整资源,全部更新)
- PATCH :在服务器更新资源(客户端提供改变的属性,局部更新)
- DELETE :从服务器删除资源
- HEAD:和GET一样,只是只返回响应首部,不返回响应体,用于确认资源的信息
- OPTIONS:查询支持的方法,复杂请求的预检会用到。
比如:
- GET /zoos:列出所有动物园
- POST /zoos:新建一个动物园
- GET /zoos/ID:获取某个指定动物园的信息
- PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
- PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
- DELETE /zoos/ID:删除某个动物园
- GET /zoos/ID/animals:列出某个指定动物园的所有动物
- DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
状态码
HTTP状态码负责表示客户端HTTP请求的返回结果,标记服务端的处理是否正常,通知出现的错误等工作。
状态码类别
状态码 | 类别 | 原因短语 |
---|---|---|
1XX | Informational 信息性状态码 | 接收的请求正在处理 |
2XX | Success 成功状态码 | 请求正常处理完毕 |
3XX | Redirection 重定向状态码 | 需要进行附加操作以完成请求 |
4XX | Client Error 客户端错误状态码 | 服务器无法处理请求 |
5XX | Server Error 服务器错误状态码 | 服务器处理请求出错 |
常用状态码一览
- 200 OK:客户端发来的请求在服务端被正常处理了。
- 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
- 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
- 204 NO CONTENT :请求已成功处理,但响应中不包含响应体。比如 请求方式为[DELETE]时,表示用户删除数据成功。
- 206 Partial Content: 服务器成功执行了客户端的范围请求。响应中包含由Content-Range首部字段指定范围的实体内容
- 301 Moved Permanently: 永久性重定向,请求的资源已被分配了新的URI。应该按Location首部字段提示的URI访问。
- 302 Found, 303 See Other, 307 Temporary Redirect 都是临时性重定向,请求的资源已被临时分配了新的URI,希望用户本次使用新的URI访问。标准不太统一,每种浏览器可能出现不同的情况,了解即可。
- 304 Not Modified: 这个比较特殊,和重定向没有关系,表示服务器资源未改变,可直接使用客户端缓存。
- 400 Bad Request:用户发出的请求报文中存在语法错误,需要修改请求内容后再发送。
- 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
- 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
- 404 NOT FOUND - [*]:服务器无法找到请求的资源。或者在服务器拒绝请求且不想说明理由时使用
- 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
- 410 Gone - [GET]:用户请求的资源被永久删除,且不会再得到的。
- 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
- 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,可能外web应用存在bug。
- 503 Service Unavailable: 服务器正忙
状态码有限,可以再约定code,表示更细的状态:
def get(self, request, *args, **kwargs):res = {'code': 1001, 'error': None}try:print('do something...')except Exception as e:res['error'] = str(e)return JsonResponse(res, status=500)
错误处理
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{error: "Invalid API key"
}
提供error key,显示详细错误信息
过滤
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果
- ?limit=1:指定返回记录的数量
- ?offset=10:指定返回记录的开始位置
- ?page=2$per_page=10:指定第几页,以及每页的记录数
- ?sortby=name$order=asc:指定返回结果按照哪个属性排序,以及排序顺序
- ?id=10:指定筛选条件
返回结果
针对不同操作,服务器向用户返回的结果应该符合以下规范。
- GET /collection:返回资源对象的列表(数组)
- GET /collection/resource:返回单个资源对象
- POST /collection:返回新生成的资源对象
- PUT /collection/resource:返回完整的资源对象
- PATCH /collection/resource:返回完整的资源对象
- DELETE /collection/resource:返回一个空文档
Hypermedia
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。
比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。
{"link": {"rel": "collection https://www.example.com/zoos","href": "https://api.example.com/zoos","title": "List of zoos","type": "application/vnd.yourformat+json"
}}
上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。
Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表:
{"current_user_url": "https://api.github.com/user","authorizations_url": "https://api.github.com/authorizations",# ...
}
从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果:
{"message": "Requires authentication","documentation_url": "https://developer.github.com/v3"
}
上面代码表示,服务器给出了提示信息,以及文档的网址。
Django REST framework
通过Django本身也可以实现API设计,只是相对要麻烦些。Django REST framework基于Django进行了丰富,能更方便的实现API设计。
基本使用
settings
INSTALLED_APPS = [# ...'rest_framework',
]
路由
urlpatterns = [url(r'user/$', views.UserView.as_view()), # GET, POSTurl(r'user/(?P<pk>\d+)/$', views.UserView.as_view()), # PUT, DELETE
]
视图
from rest_framework.views import APIView
from django.http import JsonResponseclass UsersView(APIView):def dispatch(self, request, *args, **kwargs):"""请求到来之后,首先执行dispatch方法,dispatch方法根据请求方式的不同,反射执行 get/post/put等方法"""return super().dispatch(request, *args, **kwargs)def get(self, request, *args, **kwargs):res = {'code': '10001','data': [], # 字典元素'error': None}# return HttpResponse(json.dumps(res), status=200, content_type='application/json')# 如果是HttpResponse,需要手动json, 并且指定content_typereturn JsonResponse(res, status=200)def post(self, request, *args, **kwargs):passdef put(self, request, *args, **kwargs):pk = kwargs.get('pk') # 获取url命名分组传参passdef delete(self, request, *args, **kwargs):pass
生命周期
中间件
路由系统
- .as_view() 方法:
return csrf_exempt(view)
- .as_view() 方法:
CBV视图类
执行dispatch方法
二次封装request
def initialize_request(self, request, *args, **kwargs):parser_context = self.get_parser_context(request) # return Request(request,parsers=self.get_parsers(), # 解析器authenticators=self.get_authenticators(), # 认证negotiator=self.get_content_negotiator(), # 选择器parser_context=parser_context # 字典:view和参数)
try:
获取版本,认证,权限,节流
def initial(self, request, *args, **kwargs):"""Runs anything that needs to occur prior to calling the method handler."""self.format_kwarg = self.get_format_suffix(**kwargs)# Perform content negotiation and store the accepted info on the request# 根据用户请求选择neg = self.perform_content_negotiation(request)request.accepted_renderer, request.accepted_media_type = neg# Determine the API version, if versioning is in use.# 获取版本信息,和处理版本的类的对象version, scheme = self.determine_version(request, *args, **kwargs)request.version, request.versioning_scheme = version, scheme# Ensure that the incoming request is permitted# 认证self.perform_authentication(request)# 权限self.check_permissions(request)# 控制访问次数(每天访问10次)self.check_throttles(request)
根据请求方法反射执行 GET/POST/DELETE…
except:
- 处理异常
返回响应
版本
查看源码可知,Django REST framework一共支持5种版本控制方式:
- AcceptHeaderVersioning
- URLPathVersioning
- NamespaceVersioning
- HostNameVersioning
- QueryParameterVersioning
导入及使用方式:
from rest_framework.versioning import URLPathVersioningclass TestView(APIView):versioning_class = URLPathVersioning # 指定版本pass
版本控制中通用的settings全局配置:
REST_FRAMEWORK = {# 'DEFAULT_VERSION': 'v1', # 默认版本# 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允许的版本
}
下面介绍其中两种比较常用获取版本的方式。
基于查询字符串传参
settings配置
REST_FRAMEWORK = {'VERSION_PARM': 'version' # 配置从URL中获取值的key
}
urls配置
urlpatterns = [url(r'test', views.TestView.as_view(), name='test')
]
CBV
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import QueryParameterVersioningclass TestView(APIView):versioning_class = QueryParameterVersioning # 指定版本def get(self, request, *args, **kwargs):# 获取版本print(request.version)# 获取版本管理的类print(request.versioning_scheme)# 反向生成urlreverse_url = request.versioning_scheme.reverse('test', request=request)print(reverse_url)return Response('get xxxxxx')"""
浏览器访问:http://127.0.0.1:8866/test/?version=v1
打印结果:
v1
<rest_framework.versioning.QueryParameterVersioning object at 0x0000024779F1A278>
http://127.0.0.1:8866/test?version=v1
"""
url分组传参
urls
urlpatterns = [url(r'test/(?P<version>[v1|v2]+)/$', views.TestView.as_view(), name='test')
]
# 传参必须是 v1 或 v2
CBV
更换 versioning_class 为 URLPathVersioning 即可
from rest_framework.versioning import URLPathVersioningclass TestView(APIView):versioning_class = URLPathVersioning # 指定版本def get(self, request, *args, **kwargs):pass"""
浏览器访问:http://127.0.0.1:8866/test/v2/
"""
认证
REST framework自带了认证方式:
- BasicAuthentication # 基本认证
- SessionAuthentication # 基于django request 对象的用户session
- TokenAuthentication # 基于rest自带的Token model,
- RemoteUserAuthentication # 基于request 请求头中的用户信息
以及它们的基类 BaseAuthentication,通过派生BaseAuthentication 并实现其中的方法,我们可以自定义认证类,下面我们先简单体会一下
from rest_framework.views import APIView
from django.http import JsonResponse
from rest_framework.response import Response
# 认证相关
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptionsTOKEN_LIST = [ #定义token,稍后我们会随机生成它'hello','world'
]class CustomAuthentication(BaseAuthentication):"""自定义认证类"""def authenticate(self, request): # 接口约束token = request._request.GET.get('tk')if token in TOKEN_LIST:return ('lena', None)# return None # 支持匿名用户raise exceptions.AuthenticationFailed('认证失败') # 不允许匿名用户,交给dispatch中的异常处理class TestView(APIView):versioning_class = URLPathVersioning # 指定版本authentication_classes = [CustomAuthentication,] # 指定认证方式;def dispatch(self, request, *args, **kwargs):return super().dispatch(request, *args, **kwargs)def get(self, request, *args, **kwargs):passreturn Response('get xxxxxx')# http://127.0.0.1:8866/test/v2/?tk=hello --> 认证成功
# http://127.0.0.1:8866/test/v2/?tk=hello888 --> 认证失败 HTTP 403 Forbidden (dispatch中异常处理返回值)
正经使用
和model关联起来,根据用户名实时生成token,用户登录成功后,拿着token访问需要认证的api
创建model表并迁移
from django.db import modelsclass UserInfo(models.Model):username = models.CharField(max_length=32)password = models.CharField(max_length=64)email = models.EmailField()user_type_choices = [(1, '普通用户'),(2, '版主'),(3, '管理员'),]user_type = models.IntegerField(choices=user_type_choices, default=1)class Token(models.Model):user = models.OneToOneField(to=UserInfo) # 一对一关系token = models.CharField(max_length=64)
说明:
- 两张表一对一的关系只能在一个设备上登录。新设备上登录后,服务端生成新的token 会覆盖旧的token值(具体看下面AuthView中的逻辑),导致原先使用旧token的设备无法访问api,除非重新登录,获取新的token。如果设置成一对多关系,那么可以支持多设备登录。(当然O2O 的情况下,在设备间拷贝token过去也可以实现)
新增登陆路由和登录认证
urls
urlpatterns = [url(r'api/(?P<version>[v1|v2]+)/auth/$', views.AuthView.as_view(), name='auth') # 登录认证
]
CBV登录认证
def generate_token(username):"""根据用户名和时间,进行MD5值"""import timeimport hashlibmd5 = hashlib.md5(username.encode('utf-8'))md5.update(str(time.time()).encode('utf-8'))return md5.hexdigest()class AuthView(APIView):def post(self, request, *args, **kwargs):res = {'code': 1000, # code: 1000 登录成功;1001登录失败'msg': None, # 错误信息'token': None}username = request._request.POST.get('username') pwd = request._request.POST.get('pwd')print('usernaem:',username)print('pwd:',pwd)user_obj = models.UserInfo.objects.filter(username=username, password=pwd).first()if user_obj:# 如果用户存在,那么生成token并更新token = generate_token(username)models.Token.objects.update_or_create(user=user_obj, defaults={'token': token})res['token'] = tokenelse:res['code'] = 1001res['msg'] = '用户名或密码错误'return JsonResponse(res)
说明:
- 因为是作为api, 只需要post方法即可,登录页面由前端处理
- request在dispatch中经过了二测封装,通过request._request获取原来的request对象
- 封装后的request提供了
query_params
属性访问request._request.GET
,data
属性访问request._request.POST
- 更新/创建 token:update_or_create,user=user_obj是筛选条件,存在则用default更新(比如用户换了登录设备),不存在则创建;
自定义认证类并给api使用
class CustomAuthentication(BaseAuthentication):def authenticate(self, request):token = request.query_params.get('tk')token_obj = models.Token.objects.filter(token=token).first()if token_obj:# 返回(用户对象,token对象)return (token_obj.user, token_obj)# return None # 支持匿名用户raise exceptions.AuthenticationFailed('认证失败') # 不允许匿名用户,交给dispatch中的异常处理class TestView(APIView):versioning_class = URLPathVersioning # 指定版本authentication_classes = [CustomAuthentication, ] # 指定认证方式def dispatch(self, request, *args, **kwargs):return super().dispatch(request, *args, **kwargs)def get(self, request, *args, **kwargs):# 认证Ok, 打印用户信息print(request.user.username)print(request.user.email)return Response('get xxxxxx')""" 认证成功,打印出用户信息
lena
lena@live.com
"""
说明:TestView中之所以能request.user.username
,是因为认证类对象执行authenticate
方法返回的元组,被赋值给了Request对象:self.user, self.auth = user_auth_tuple
通过requests模块模拟登录提交
import requestsapi = 'http://127.0.0.1:8866/api/v1/auth/'
response = requests.post(url=api, data={'username': 'lena', 'pwd': '123'})
print(response.text)
"""
{"code": 1000, "msg": null, "token": "117d16c0b1c9397a0573c28b67dad6f8"}
"""
访问api,认证成功,收到服务端返回的信息,其中包括token,以后只需要携带token就可以访问需要认证的api
requests模拟访问api
用之前登录成功返回的token访问目标api
api = 'http://127.0.0.1:8866/api/v1/test/'
response2 = requests.get(url=api, params={'tk': '117d16c0b1c9397a0573c28b67dad6f8'})
print(response2.text)"""
get xxxxxx
"""
认证的几种配置方式
局部配置
在CBV类中通过authentication_classes = [CustomAuthentication, ]
指定,比如上面例子中的做法。
多继承 – 推荐
并不是所有的api都需要作认证,比如登录。因此可以通过写一个基类(指定认证类),让需要认证的api首先继承这个基类即可:
# 基类
class Token_auth(APIView):authentication_classes = [CustomAuthentication, ] # 指定认证方式# 需要认证的api 首先继承基类
class TestView(Token_auth, APIView):versioning_class = URLPathVersioning # 指定版本pass# 不需要认证的api, 不继承基类
class AuthView(APIView):pass
settings全局
在settings中作全局配置,不需要认证的api指定authentication_classes = []
为空即可
REST_FRAMEWORK = {'DEFAULT_AUTHENTICATION_CLASSES': [# 自定义认证类路径'utils.authentication.CustomAuthentication', ]
}
class AuthView(APIView):authentication_classes = []
认证功能源码剖析
遵循之前的生命周期分析,进入CBV视图后,流程如下:
dispatch(self, request, *args, **kwargs)
request = self.initialize_request(request, *args, **kwargs) 二次封装request
return Request(# ...authenticators=self.get_authenticators(), # 为request对象封装认证类 )
def get_authenticators(self):
return [auth() for auth in self.authentication_classes] # 循环认证类列表,并实例化对象
self.initial(request, *args, **kwargs) 初始化
self.perform_authentication(request) 执行认证
request.user 调用request对象user方法(@property装饰)(登录之后存在request.user 同django默认设计)
self._authenticate()
def _authenticate(self):"""执行每个认证对象的认证方法:一旦异常raise 全部终止,交由dispatch中的异常处理如果返回元组,赋值给request.user, request.auth, 并return 后续不再执行如果既没有异常,又没有返回,执行_not_authenticated() 匿名用户"""for authenticator in self.authenticators:try:user_auth_tuple = authenticator.authenticate(self)except exceptions.APIException:self._not_authenticated()raiseif user_auth_tuple is not None:self._authenticator = authenticatorself.user, self.auth = user_auth_tuplereturnself._not_authenticated()
执行自带认证类或自定义认证类中authenticate方法
class CustomAuthentication(BaseAuthentication):def authenticate(self, request):token = request.query_params.get('tk')token_obj = models.Token.objects.filter(token=token).first()if token_obj:# 返回(用户对象,token对象)return (token_obj.user, token_obj)# return None # 支持匿名用户,将执行 self._not_authenticated()raise exceptions.AuthenticationFailed('认证失败') # 不允许匿名用户,交给dispatch中的异常处理
匿名用户
def _not_authenticated(self):"""为未认证的请求设置authenticator, user & authtoken默认值分别是 None, AnonymousUser & None,后两个可以在settings中配置"""self._authenticator = Noneif api_settings.UNAUTHENTICATED_USER: # 默认配置中会使用django内置的AnonymousUser类self.user = api_settings.UNAUTHENTICATED_USER()else:self.user = Noneif api_settings.UNAUTHENTICATED_TOKEN:self.auth = api_settings.UNAUTHENTICATED_TOKEN()else:self.auth = None# 匿名用户settings相关配置REST_FRAMEWORK = {'UNAUTHENTICATED_USER': None, # 取消匿名用户'UNAUTHENTICATED_TOKEN': None, }
如果认证类的
authenticate
方法执行了returen None
,导致user_auth_tuple
为空,进而执行self._not_authenticated()
方法时,将默认产生一个匿名用户。那么request.user
非空,而是一个匿名用户对象。如果希望取消对匿名用户的支持,就需要在settings中指定'UNAUTHENTICATED_USER': None
,来覆盖默认的匿名用户配置。
权限
分析了上面的认证后,权限的流程是一摸一样的,下面我们看一下具体用法
自定义权限类
class CustomPermission(BasePermission):message = '无权限' # 查看源码可知,可以通过message自定义提示信息def has_permission(self, request, view):"""返回True: 有权限;返回False: 无权限"""method = request._request.methodif request.user.user_type == 1 and isinstance(view, TestView) and method == 'POST': # 限制普通用户通过post方式访问TestViewreturn Falsereturn True
应用
# 方式一:局部视图
class TestView(Token_auth, APIView):versioning_class = URLPathVersioning # 指定版本permission_classes = [CustomPermission, ] # 指定权限def dispatch(self, request, *args, **kwargs):return super().dispatch(request, *args, **kwargs)def get(self, request, *args, **kwargs):print(request.user.username)print(request.user.email)return Response('get xxxxxx')def post(self, request, *args, **kwargs):return Response('post xxx')# 方式二:settings全局
REST_FRAMEWORK = {'DEFAULT_PERMISSION_CLASSES': [# path.to.Permissionclass 路径之间用点分割]
}
这样当我们通过get方法访问TestView时,将得到正常响应,如果是post,那么将得到{"detail":"无权限"}
的响应。
请求次数限制
配置
REST_FRAMEWORK = {"DEFAULT_THROTTLE_RATES": {'anon': '5/m', # scope: rate 匿名用户: 每分钟5次'user': '10/m' # 登录用户}}
自定义访问控制类
from rest_framework.throttling import SimpleRateThrottle# 根据request.user 判断匿名不匿名 (在每次进来时认证中赋值了用户或None)class Custom_anno_control(SimpleRateThrottle):"""匿名用户控制,用默认get_ident,获取ip作为标识"""scope = 'anon' # 决定settings中DEFAULT_THROTTLE_RATES 的keydef allow_request(self, request, view):if request.user: # 如果是登录用户,不限制return Trueself.key = self.get_cache_key(request, view)print('key=====',self.key)self.history = self.cache.get(self.key, [])self.now = self.timer()while self.history and self.history[-1] <= self.now - self.duration:self.history.pop()if len(self.history) >= self.num_requests:return self.throttle_failure()return self.throttle_success()def get_cache_key(self, request, view):return self.cache_format % {'scope': self.scope,'ident': self.get_ident(request)}class Custom_user_control(SimpleRateThrottle):"""登录用户控制,直接用用户名+CBV视图类名作为标识"""scope = 'user'def allow_request(self, request, view):if not request.user: # 如果是匿名用户,不限制return Trueself.key = request.user.username + view.__class__.__name__ # 如果登录用户,用用户名和类名作为标识if self.key is None:return Trueself.history = self.cache.get(self.key, [])self.now = self.timer()while self.history and self.history[-1] <= self.now - self.duration:self.history.pop()if len(self.history) >= self.num_requests:return self.throttle_failure()return self.throttle_success()
这里参考源码稍作修改:
- Custom_anno_control中不限制登录用户,Custom_user_control不限制匿名用户,保证CBV在同时应用二者时,不用关心调用顺序。
- Custom_user_control中的key采用用户名拼接CBV视图类名,确保访问次数限制能精确到具体CBV视图类,而不是所有CBV一共能访问多少次。
自定义权限
class CustomPermission(BasePermission):message = '无权限' # 查看源码可知,可以通过message自定义提示信息def has_permission(self, request, view):"""返回True: 有权限;返回False: 无权限"""if not request.user: # 仅允许登录用户,限制匿名用户return Falsereturn True
CBV中应用
class IndexView(APIView):"""控制登录用户访问频次:10/m, 匿名用户访问频次5/m"""authentication_classes = [CustomAuthentication, ] # 获取登录token和用户;如果不认证限制,那么无法自动获取token,都是匿名访问throttle_classes = [Custom_anno_control, Custom_user_control]# 要同时允许登录用户和匿名用户的访问并作限制,必须同时指定authentication_classes认证类和throttle_classes访问控制类:# 如果用户登录,那么拿着token,可以访问配置中指定的次数 'user': '10/m'# 如果用户未登录,那么没有token或者token错误,可以访问配置中指定的数 'anon': '5/m',def get(self, request, *args, **kwargs):return HttpResponse('欢迎访问首页')class ShoppingView(APIView):versioning_class = URLPathVersioning # 指定版本authentication_classes = [CustomAuthentication, ] # 认证(认证可能同时允许登录用户和匿名用户)permission_classes = [CustomPermission, ] # 指定权限,这里作二次限制,即便匿名用户通过了认证,也过不了权限throttle_classes = [Custom_user_control, ] # 限制登录用户的访问次数def dispatch(self, request, *args, **kwargs):return super().dispatch(request, *args, **kwargs)def get(self, request, *args, **kwargs):print(request.user.username)print(request.user.email)return HttpResponse('购物车 访问')def post(self, request, *args, **kwargs):return HttpResponse('购物车 提交')
解析器
根据请求头中的content-type,对内容进行解析。在执行request.data时触发。
'DEFAULT_PARSER_CLASSES': ('rest_framework.parsers.JSONParser', # content-type: application/json'rest_framework.parsers.FormParser', # content-type: application/x-www-form-urlencoded'rest_framework.parsers.MultiPartParser' # content-type: multipart/form-data(可以在form中同时上传数据和文件)
),
默认同时支持以上三种解析器(源码中通过for 循环一一匹配请求头的content-type),还有一个FileUploadParser (只能上传文件,鸡肋)。如果想配置的话可以在CBV中指定parser_classes=[]
,或者在配置中配置,没啥必要,默认都配置上了,除非你闲的蛋疼。。。
序列化
对于序列化,有两种方案,一种是将查询结果通过.value_list('field1', 'field2','xxx')
这种方式,返回QuerySet包字典的格式,然后转化为列表:
queryset = models.UserInfo.objects.all().values_list('id', 'name')
res = list(queryset)
这种方式有个弊端,无法处理choice,M2M字段的情况。
第二种方案就是这里的序列化类,REST中内置了三种:
- Serializer
- ModelSerializer
- HyperlinkedModelSerializer
作用
- 对数据库查询结果进行序列化,返回json数据
- 验证用户提交,类似Django中的Form / ModelForm
序列化:
from rest_framework.response import Response # 对于序列化的结果需要用Response对象才能正确返回结果
from rest_framework import serializers
from rest_framework.serializers import Serializer
from rest_framework.serializers import ModelSerializer
from rest_framework.serializers import HyperlinkedModelSerializer# 派生Serializer类
# 两种方式:Serializer 和 ModelSerializer (相当与Django中的Form和ModelForm)
class UserSerializer(Serializer):username = serializers.CharField()password = serializers.CharField()email = serializers.EmailField()user_type = serializers.IntegerField()group = serializers.CharField(source="group.title", required=False) # 通过source指定FK对象的显示class UserModelSerializer(ModelSerializer):class Meta:model = models.UserInfofields = '__all__'depth = 2# depth = 0 或 1, 只显示FK的PK, 如果=2,可以显示FK对象的字段,比如下面的group外键;# [{"id":1,"username":"Lena",..."group":{"id":1,"title":"A组"}},# 如果外键嵌套很多,depth深度过深可能会影响性能。。# 返回json数据
class SerializerView(APIView):def get(self, request, *args, **kwargs):user_list = models.UserInfo.objects.all()user_obj = models.UserInfo.objects.all().first()ser = UserSerializer(instance=user_list, many=True) # 返回queryset序列化对象时,many=True# ser = UserModelSerializer(instance=user_list, many=True)# ser = UserSerializer(instance=user_obj, many=False) # 返回单个序列化对象时,many=Falsereturn Response(ser.data)
定制序列化结果
对于choice字段,外键或者多对多等跨表字段,需要自定制
from rest_framework import serializers# 假设CourseDetail 和 Course 表是一对一关系
class CourseDetailSerializer(serializers.ModelSerializer):"""课程详情"""course_name = serializers.CharField(source='course.name') # O2O跨表recommend_courses = serializers.SerializerMethodField() # 写一个函数 def get_field(self, obj),返回的结果就是该字段的结果price_policy = serializers.SerializerMethodField()class Meta:model = models.CourseDetailfields = ['id', 'course_name', 'recommend_courses']def get_recommend_courses(self, obj): # obj指当前表CourseDetail中的一条记录"""获取M2M字段的结果"""ret = []recommend_courses_list = obj.recommend_courses.all()for item in recommend_courses_list:ret.append({'id': item.id, 'name': item.name})return retdef get_price_policy(self, obj):"""获取choice字段的结果"""ret = []price_policy = obj.course.price_policy.all()for item in price_policy:ret.append({'valid_period': item.get_valid_period_display(), 'price': item.price})return ret# 也可以继承派生字段类型,只需要重写get_attribute 和 to_representation 方法即可
class MtoMField(serializers.CharField):def get_attribute(self, instance):return instance.objects.values('name','title')def to_representation(self,value):return list(value)class MyField(serializers.CharField):def get_attribute(self, instance):#instance 是数据库对应的每行数据,即model 实例对象data_list = instance.recommend_courses.all()return data_listdef to_representation(self, value):ret = []for row in value:ret.append({'id': row.id, 'name': row.name})return ret
hypermedia相关
如果希望序列化的结果包括相关的链接关系,那么需要在序列化对象时提供当前的request,这里只需要提供一个上下文参数context即可实现。注意路由需要传id
urlpatterns = [url(r'test/(?P<pk>\d+)/', views.TestView.as_view(), name='test'),
]
class ModelUserSerializer(serializers.ModelSerializer):ut = serializers.HyperlinkedIdentityField(view_name='test')class Meta:model = models.UserInfofields = "__all__"class TestView(APIView):def get(self, request, *args, **kwargs):data_list = models.UserInfo.objects.all()ser = ModelUserSerializer(instance=data_list, many=True, context={'request': request})return Response(ser.data)
from rest_framework import serializers
from rest_framework.response import Responseclass UserSerialize(serializers.HyperlinkedModelSerializer):class Meta:model = models.UserInfofields = ['user','pwd','id','url']class SerializeView(APIView):def get(self, request, *args, **kwargs):user_list = models.UserInfo.objects.all()ser = UserSerialize(instance=user_list,many=True,context={'request': request})return Response(ser.data)def post(self, request, *args, **kwargs):ser = UserSerialize(data=request.data)if ser.is_valid():print(ser.validated_data)print(request.data)return Response(ser.validated_data)else:return Response(ser.errors)
验证用户提交
# 自定义验证类, 在__call__中写验证逻辑
class PasswordValidator:def __init__(self):passdef __call__(self, value):if value != '123':raise serializers.ValidationError('密码必须是123')def set_context(self, serializer_field):pass# 派生Serializer,同样有两种方式
# 两种方式:Serializer 和 ModelSerializer (相当与Django中的Form和ModelForm)
class UserSerializer(Serializer):username = serializers.CharField(min_length=6)password = serializers.CharField(validators=[PasswordValidator(),])email = serializers.EmailField()user_type = serializers.IntegerField()group = serializers.CharField(source="group.title", required=False) # 通过source指定FK对象的显示class UserModelSerializer(ModelSerializer):# username = serializers.CharField(min_length=6) 相当于下面的extra_kwargs# password = serializers.CharField(validators=[PasswordValidator(), ])class Meta:model = models.UserInfofields = '__all__'extra_kwargs = {'username': {'min_length': 6},'password': {'validators: [PasswordValidator(),]'}}class SerializerView(APIView):def post(self, request, *args, **kwargs):ser = UserSerializer(data=request.data)if ser.is_valid():print(ser.validated_data)else:print(ser.errors)return Response('got post .....')
post提交数据
api = 'http://127.0.0.1:8899/api/v1/ser/'
response = requests.post(url=api, data={'username':'sebastian', 'password':123, 'email':'asb'})
通过requests模块模拟post提交,CBV中打印结果如下:
{'email': ['Enter a valid email address.'], 'user_type': ['This field is required.']}
分页器
PageNumberPagination 页码分页
分页器类不能直接使用,需要继承它并指定参数
urlpatterns = [ url(r'api/page/$', views.PageTestView.as_view())]
from rest_framework.pagination import PageNumberPagination
from rest_framework import serializersclass CustomPagination(PageNumberPagination):# http://api.example.org/accounts/?page=4&page_size=100# 指定客户端query_param参数:每页数据大小 和 页码page_size_query_param = 'page_size'page_query_param = 'page'# 定制每页显示多少条数据(默认为None, 最终取决于请求中的查询参数) 以及最大值page_size = 10max_page_size = 20class UserSerializer(serializers.ModelSerializer):class Meta:model = models.UserInfofields = "__all__"class PageTestView(APIView):def get(self, request, *args, **kwargs):user_list = models.UserInfo.objects.all()# 实例化分页对象,并根据请求参数,获取分页数据paginator = CustomPagination()page_user_list = paginator.paginate_queryset(user_list, request, view=self)# 序列化分页数据ser = UserSerializer(instance=page_user_list, many=True)# 获取分页响应(可额外生成上一页/下一页链接)response = paginator.get_paginated_response(ser.data)return response
可能会报如下警告,对于无序的数据,分页器生成的分页数据可能不一致:
UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list
因此在获取数据库数据时,可以做一下排序,这样就不会报警告了:
user_list = models.UserInfo.objects.all().order_by('id')
通过requests模块对该api发起请求,将得到如下结果:
{"count":2,"next":"http://127.0.0.1:8899/api/page/?page=2&page_size=1","previous":null,"results":[{"id":1,"username":"Lena","password":"123","email":"lena@live.com","user_type":1,"group":1}]}
LimitOffsetPagination 位置分页
class CustomPagination(LimitOffsetPagination):# http://api.example.org/accounts/?offset=400&limit=100limit_query_param = 'limit'offset_query_param = 'offset'max_limit = Nonedefault_limit = 10
CursorPagination 游标分页
对于以上两种分页方式,都存在性能问题,页码往后翻的越多,速度越慢。即便是offset,每次也要从头扫描,因此如果每次都能从上一次索引位置继续的话,就可以解决性能下降的问题。看下面的几种情况:
select * from tb where dept = 'it'
select * from tb where dept = 'it' limit 1 # 性能高
加入limit,找到1条就不找了,否则找完整个表,速度自然慢。
select * from tb offset 0 limit 5
select * from tb offset 100 limit 5
select * from tb offset 1000 limit 5
...
select * from tb where id>1000 offset 0 limit 5 # 性能高
通过id筛选,跳过前面的,这样就不用从头扫描。这就是cursor游标分页的原理。cursor分页每一次从上一次索引位置继续,因此只能上一页,下一页,不能直接跳转页码。
class CustomPagination(CursorPagination):# URL传入的游标参数cursor_query_param = 'cursor'# 默认每页显示的数据条数page_size = 1# URL传入的每页显示条数的参数page_size_query_param = 'page_size'# 每页显示数据最大条数max_page_size = 1000# 根据ID从大到小排列ordering = "id"
通过requests模块访问,结果如下
api = 'http://127.0.0.1:8899/api/page/?page_size=1'
response = requests.get(url=api)
print(response.text)
"""
{"next":"http://127.0.0.1:8899/api/page/?cursor=cD0x&page_size=1","previous":null,"results":[{"id":1,"username":"Lena","password":"123","email":"lena@live.com","user_type":1,"group":1}]}
"""
可以看到cursor=cD0x
是加密,看不到第几页,只能一页页翻,没办法通过指定cursor的值直接翻页。
路由和视图
如果要支持url带后缀,比如.json
,那么可以在路由规则后面加\.(?P<format>\w+)$
(具体见后面的渲染器部分)。下面通过实现增删改查视图,来看看几种不同路由方式的区别。
增删改查分别对应几种不同请求方法:
- GET: 查询列表
- POST: 增加
- GET: 查询单条数据(id)
- PUT: 更新(id)
- DELETE: 删除(id)
手动路由
需要写两套路由,以分别支持无id和有id传参的情况,每套路由还要支持无url后缀和有url后缀的情况,共计4条路由。推荐手动路由,可定制性强。
urlpatterns = [# http: //127.0.0.1:8000/api/router 无id# GET: 查询(列表)# POST: 增加url(r'api/router/$', views.RouterView.as_view()),url(r'api/router\.(?P<format>\w+)$', views.RouterView.as_view()), # 支持后缀# http: //127.0.0.1:8000/api/router/1 有id# GET: 查询(单条记录)# PUT: 更新# DELETE: 删除url(r'api/router/(?P<pk>\d+)/$', views.RouterView.as_view()),url(r'api/router/(?P<pk>\d+)\.(?P<format>\w+)$', views.RouterView.as_view()), # 支持后缀
]
视图中手动实现这几种请求方式:
from rest_framework.serializers import ModelSerializer
from rest_framework.views import APIViewclass RouterSerializer(ModelSerializer):class Meta:model = models.UserInfofields = '__all__'class RouterView(APIView):def get(self, request, *args, **kwargs):pk = kwargs.get('pk')if pk:obj = models.UserInfo.objects.filter(pk=pk).first()ser = RouterSerializer(instance=obj, many=False)else:user_list = models.UserInfo.objects.all()ser = RouterSerializer(instance=user_list, many=True)return Response(ser.data)def post(self, request, *args, **kwargs):passdef put(self, request, *args, **kwargs):pk = kwargs.get('pk')passdef delete(self, request, *args, **kwargs):pk = kwargs.get('pk')pass
半自动路由
视图继承中继承ModelViewSet
,其中提供了增删改查方法,不过需要在路由中指定。(视图部分存疑先)
urlpatterns = [url(r'api/router/$', views.RouterView.as_view({'get': 'list', 'post': 'create'})),url(r'api/router/(?P<pk>\d+)/$', views.RouterView.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
]
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSetclass RouterSerializer(ModelSerializer):class Meta:model = models.UserInfofields = '__all__'class RouterView(ModelViewSet):queryset = models.UserInfo.objects.all()serializer_class = RouterSerializer
全自动路由
from django.conf.urls import url,include
from app01 import views
from rest_framework.routers import DefaultRouter # 自动路由router = DefaultRouter() # 实例化router对象
router.register(r'/XXX/', views.TargetView1) # 将目标视图注册到router对象上
router.register(r'/XXY/', views.TargetView2) # 可以注册多个urlpatterns = [url(r'^', include(router.urls)), # 自动实现增删改路由
]
from rest_framework.viewsets import ModelViewSet
from rest_framework import serializersclass RouteSerializer(serializers.ModelSerializer):class Meta:model = models.UserInfofields = "__all__"class RouteView(ModelViewSet):queryset = models.UserInfo.objects.all()serializer_class = RouteSerializer
渲染器
根据 用户请求URL 或 用户可接受的类型,筛选出合适的 渲染组件。注意,如果要支持url后缀,路由正则后面必须加\.(?P<format>\w+)
。如果同时多个存在时,自动根据URL后缀来选择渲染器。
urlpatterns = [url(r'test/$', views.RenderTestView.as_view()),url(r'test\.(?P<format>\w+)$', views.RenderTestView.as_view()), # 支持后缀
]
json
用户请求url
http://127.0.0.1:8000/test/?format=json
http://127.0.0.1:8000/test.json
http://127.0.0.1:8000/test
CBV
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from rest_framework import serializers
from rest_framework.renderers import JSONRendererclass CustomPagination(PageNumberPagination):passclass RenderTestSerializer(serializers.ModelSerializer):passclass RenderTestView(APIView):renderer_classes = [JSONRenderer, ]def get(self, request, *args, **kwargs):pass
表格
以table表友好地呈现 ,好看,没多大用。
用户访问url
http://127.0.0.1:8000/test/?format=admin
http://127.0.0.1:8000/test.admin
http://127.0.0.1:8000/test/
CBV
from rest_framework.renderers import HTMLFormRendererclass RenderTestView(APIView):renderer_classes = [HTMLFormRenderer, ]def get(self, request, *args, **kwargs):pass
Form表单
form表单,只能返回单个序列化对象,否则报错,没暖用。
用户访问url
http://127.0.0.1:8000/test/?format=admin
http://127.0.0.1:8000/test.admin
http://127.0.0.1:8000/test/
CBV
from rest_framework.renderers import HTMLFormRendererclass RenderTestView(APIView):renderer_classes = [HTMLFormRenderer, ]def get(self, request, *args, **kwargs):pass
浏览器格式API+JSON
这种是最常用的
用户访问url
http://127.0.0.1:8000/test/?format=api
http://127.0.0.1:8000/test.json
http://127.0.0.1:8000/test/
CBV
from rest_framework.renderers import JSONRenderer
from rest_framework.renderers import BrowsableAPIRendererclass RenderTestView(APIView):renderer_classes = [JSONRenderer, BrowsableAPIRenderer, ]def get(self, request, *args, **kwargs):pass
Django REST framework API开发相关推荐
- Django REST framework API 指南(2):响应
Django REST framework API 指南(1):请求 Django REST framework API 指南(2):响应 Django REST framework API 指南(3 ...
- Django REST framework API 指南(12):验证器
官方原文链接 本系列文章 github 地址 转载请注明出处 验证器 大多数情况下,您在 REST framework 中处理验证时,只需依赖默认的字段验证,或者在序列化类或字段类上编写明确的验证方法 ...
- Django REST Framework API Guide 01
之前按照REST Framework官方文档提供的简介写了一系列的简单的介绍博客,说白了就是翻译了一下简介,而且翻译的很烂.到真正的生产时,就会发现很鸡肋,连熟悉大概知道rest framework都 ...
- django如何调用php接口,使用django集成第三方api开发接口注意事项
重要概念 如果您计划将Django应用程序与第三方REST API集成,请务必记住以下几点: 使用API很慢. 我们必须仔细实现它们,因为它是在服务器端执行的额外HTTP请求,因此它可以大大增加请求/ ...
- Django REST Framework API Guide 02
本节大纲 1.Generic Views 2.ViewSets 1.Generic Views CBV的主要的一个优点就是极大的允许了对于代码的从用.自然,rest framework取其优势,提供 ...
- Django REST framework API 指南(11):序列化·关系
官方原文链接 本系列文章 github 地址 转载请注明出处 Serializer 关系 关系字段用于表示模型关系. 它们可以应用于 ForeignKey,ManyToManyField 和 OneT ...
- Django REST Framework API Guide 07
本节大纲 1.Permissions 2.Throttling Permissions 权限是用来授权或者拒绝用户访问API的不同部分的不同的类的.基础的权限划分 1.IsAuthenticated ...
- Django REST framework API 指南(25):状态码
官方原文链接 本系列文章 github 地址 转载请注明出处 状态码 不建议在你的响应中使用裸露(直接使用数字)的状态码. REST framework 包含一组命名常量,你可以使用它们使代码更加清晰 ...
- django restful 请求_利用 Django REST framework 构建 RESTful Web API
利用 Django REST framework 构建 RESTful Web API 终于到了动手操作的环节啦,这一节,我们以师生管理系统为例,带领大家搭建一套 framework Web API. ...
最新文章
- USTC English Club Note20211108
- 设置mysql的字符编码解决中文乱码问题
- Linux下显示ip所属位置
- spss方差分析_【案例】SPSS统计分析:多因素方差分析
- java 单例 生命周期_Rhythmk 一步一步学 JAVA (13) Spring-2 之Ben懒加载以及生命周期,单例...
- C#中的Dictionary字典类介绍(转载)
- ES建立索引步骤, 1,index 2.mapping 3,别名
- JLabel鼠标停在上面显示小手图标 点击跳转到相应网页
- 2021 ACDU China Tour启航,首站邀您北京共话行业数据库技术实践
- jeb反编译导出Java工程_Android 反编译(JEB.android.decompiler)
- TUXEDO配置常见问题及解决方法
- 从Jira到GitHub,详解Spring Framework问题跟踪系统的迁移过程
- flask综合整理2
- 总结与归纳:深度神经网络中的数据融合方法
- 使用DFA算法对敏感词进行过滤
- spring boot中的banner制作
- java.lang.AssertionError: Activity needs to be set if initial lifecycle state is resumed
- 碎碎点点-积土成山,风雨兴焉;积水成渊,蛟龙生焉
- Coverage分析工具UNR的使用方法总结
- 印度社交市场:谁能挑战Facebook们的霸主地位?
热门文章
- poj 2262 Goldbach's Conjecture(筛素数)
- twisted系列教程十六–twisted守护进程
- MyBatis3系列__05查询补充resultMap与resultType区别
- 170329、用 Maven 部署 war 包到远程 Tomcat 服务器
- onClientClick 和 onClient 区别
- Codeforces632E Thief in a Shop(NTT + 快速幂)
- webservice 心得
- EHcache缓存框架详解
- 如何将10元店,做到月2000万流水?
- hibernate reverse engineering 中没有java src folder