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)
  • 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)

说明:

  1. 两张表一对一的关系只能在一个设备上登录。新设备上登录后,服务端生成新的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开发相关推荐

  1. Django REST framework API 指南(2):响应

    Django REST framework API 指南(1):请求 Django REST framework API 指南(2):响应 Django REST framework API 指南(3 ...

  2. Django REST framework API 指南(12):验证器

    官方原文链接 本系列文章 github 地址 转载请注明出处 验证器 大多数情况下,您在 REST framework 中处理验证时,只需依赖默认的字段验证,或者在序列化类或字段类上编写明确的验证方法 ...

  3. Django REST Framework API Guide 01

    之前按照REST Framework官方文档提供的简介写了一系列的简单的介绍博客,说白了就是翻译了一下简介,而且翻译的很烂.到真正的生产时,就会发现很鸡肋,连熟悉大概知道rest framework都 ...

  4. django如何调用php接口,使用django集成第三方api开发接口注意事项

    重要概念 如果您计划将Django应用程序与第三方REST API集成,请务必记住以下几点: 使用API很慢. 我们必须仔细实现它们,因为它是在服务器端执行的额外HTTP请求,因此它可以大大增加请求/ ...

  5. Django REST Framework API Guide 02

    本节大纲 1.Generic Views 2.ViewSets  1.Generic Views CBV的主要的一个优点就是极大的允许了对于代码的从用.自然,rest framework取其优势,提供 ...

  6. Django REST framework API 指南(11):序列化·关系

    官方原文链接 本系列文章 github 地址 转载请注明出处 Serializer 关系 关系字段用于表示模型关系. 它们可以应用于 ForeignKey,ManyToManyField 和 OneT ...

  7. Django REST Framework API Guide 07

    本节大纲 1.Permissions 2.Throttling Permissions 权限是用来授权或者拒绝用户访问API的不同部分的不同的类的.基础的权限划分 1.IsAuthenticated ...

  8. Django REST framework API 指南(25):状态码

    官方原文链接 本系列文章 github 地址 转载请注明出处 状态码 不建议在你的响应中使用裸露(直接使用数字)的状态码. REST framework 包含一组命名常量,你可以使用它们使代码更加清晰 ...

  9. django restful 请求_利用 Django REST framework 构建 RESTful Web API

    利用 Django REST framework 构建 RESTful Web API 终于到了动手操作的环节啦,这一节,我们以师生管理系统为例,带领大家搭建一套 framework Web API. ...

最新文章

  1. USTC English Club Note20211108
  2. 设置mysql的字符编码解决中文乱码问题
  3. Linux下显示ip所属位置
  4. spss方差分析_【案例】SPSS统计分析:多因素方差分析
  5. java 单例 生命周期_Rhythmk 一步一步学 JAVA (13) Spring-2 之Ben懒加载以及生命周期,单例...
  6. C#中的Dictionary字典类介绍(转载)
  7. ES建立索引步骤, 1,index 2.mapping 3,别名
  8. JLabel鼠标停在上面显示小手图标 点击跳转到相应网页
  9. 2021 ACDU China Tour启航,首站邀您北京共话行业数据库技术实践
  10. jeb反编译导出Java工程_Android 反编译(JEB.android.decompiler)
  11. TUXEDO配置常见问题及解决方法
  12. 从Jira到GitHub,详解Spring Framework问题跟踪系统的迁移过程
  13. flask综合整理2
  14. 总结与归纳:深度神经网络中的数据融合方法
  15. 使用DFA算法对敏感词进行过滤
  16. spring boot中的banner制作
  17. java.lang.AssertionError: Activity needs to be set if initial lifecycle state is resumed
  18. 碎碎点点-积土成山,风雨兴焉;积水成渊,蛟龙生焉
  19. Coverage分析工具UNR的使用方法总结
  20. 印度社交市场:谁能挑战Facebook们的霸主地位?

热门文章

  1. poj 2262 Goldbach's Conjecture(筛素数)
  2. twisted系列教程十六–twisted守护进程
  3. MyBatis3系列__05查询补充resultMap与resultType区别
  4. 170329、用 Maven 部署 war 包到远程 Tomcat 服务器
  5. onClientClick 和 onClient 区别
  6. Codeforces632E Thief in a Shop(NTT + 快速幂)
  7. webservice 心得
  8. EHcache缓存框架详解
  9. 如何将10元店,做到月2000万流水?
  10. hibernate reverse engineering 中没有java src folder