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, POST
    url(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, POST
    url(r'user/(?P\d+)/$', views.UserView.as_view()),  # PUT, DELETE
]

视图

from rest_framework.views import APIView
from django.http import JsonResponse

class 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_type
        return JsonResponse(res, status=200)

    def post(self, request, *args, **kwargs):
        pass

    def put(self, request, *args, **kwargs):
        pk = kwargs.get('pk')  # 获取url命名分组传参
        pass

    def 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 URLPathVersioning

class 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 QueryParameterVersioning


class TestView(APIView):
    versioning_class = QueryParameterVersioning  # 指定版本

    def get(self, request, *args, **kwargs):
        # 获取版本
        print(request.version)

        # 获取版本管理的类
        print(request.versioning_scheme)

        # 反向生成url
        reverse_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

http://127.0.0.1:8866/test?version=v1
"""

url分组传参

urls
urlpatterns = [
    url(r'test/(?P[v1|v2]+)/$', views.TestView.as_view(), name='test')
]
# 传参必须是 v1 或 v2 
CBV

更换 versioning_class 为 URLPathVersioning 即可

from rest_framework.versioning import URLPathVersioning

class 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 exceptions

TOKEN_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):
        pass
        return 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 models


class 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[v1|v2]+)/auth/$', views.AuthView.as_view(), name='auth')  # 登录认证 
]
CBV登录认证
def generate_token(username):
    """根据用户名和时间,进行MD5值"""
    import time
    import hashlib

    md5 = 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'] = token
        else:
            res['code'] = 1001
            res['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
[email protected]
"""

说明:TestView中之所以能request.user.username,是因为认证类对象执行authenticate方法返回的元组,被赋值给了Request对象:self.user, self.auth = user_auth_tuple

通过requests模块模拟登录提交
import requests

api = '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()
                raise
    
            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple
                return
    
        self._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 = None
    
        if api_settings.UNAUTHENTICATED_USER: # 默认配置中会使用django内置的AnonymousUser类
            self.user = api_settings.UNAUTHENTICATED_USER()
        else:
            self.user = None
    
        if 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.method
        if request.user.user_type == 1 and isinstance(view, TestView) and method == 'POST':  # 限制普通用户通过post方式访问TestView
            return False
        return 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 的key

    def allow_request(self, request, view):
        if request.user:  # 如果是登录用户,不限制
            return True

        self.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 True

        self.key = request.user.username + view.__class__.__name__  # 如果登录用户,用用户名和类名作为标识
        if self.key is None:
            return True

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

这里参考源码稍作修改:

  • 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 False
        return 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.UserInfo
        fields = '__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=False

        return 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.CourseDetail
        fields = ['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 ret

    def 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_list

    def 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\d+)/', views.TestView.as_view(), name='test'),
]
class ModelUserSerializer(serializers.ModelSerializer):
    ut = serializers.HyperlinkedIdentityField(view_name='test')
    class Meta:
        model = models.UserInfo
        fields = "__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 Response


class UserSerialize(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.UserInfo
        fields = ['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):
        pass

    def __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.UserInfo
        fields = '__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 serializers


class 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 = 10
    max_page_size = 20


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.UserInfo
        fields = "__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":"[email protected]","user_type":1,"group":1}]}

LimitOffsetPagination 位置分页

class CustomPagination(LimitOffsetPagination):
    # http://api.example.org/accounts/?offset=400&limit=100

    limit_query_param = 'limit'
    offset_query_param = 'offset'
    max_limit = None
    default_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":"[email protected]","user_type":1,"group":1}]}
"""

可以看到cursor=cD0x是加密,看不到第几页,只能一页页翻,没办法通过指定cursor的值直接翻页。

路由和视图

如果要支持url带后缀,比如.json,那么可以在路由规则后面加\.(?P\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\w+)$', views.RouterView.as_view()),  # 支持后缀

    # http: //127.0.0.1:8000/api/router/1 有id
    # GET: 查询(单条记录)
    # PUT: 更新
    # DELETE: 删除
    url(r'api/router/(?P\d+)/$', views.RouterView.as_view()),
    url(r'api/router/(?P\d+)\.(?P\w+)$', views.RouterView.as_view()),  # 支持后缀
]

视图中手动实现这几种请求方式:

from rest_framework.serializers import ModelSerializer
from rest_framework.views import APIView


class RouterSerializer(ModelSerializer):
    class Meta:
        model = models.UserInfo
        fields = '__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):
        pass

    def put(self, request, *args, **kwargs):
        pk = kwargs.get('pk')
        pass

    def 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\d+)/$', views.RouterView.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
]
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet


class RouterSerializer(ModelSerializer):
    class Meta:
        model = models.UserInfo
        fields = '__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 serializers

class RouteSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.UserInfo
        fields = "__all__"


class RouteView(ModelViewSet):
    queryset = models.UserInfo.objects.all()
    serializer_class = RouteSerializer

渲染器

根据 用户请求URL 或 用户可接受的类型,筛选出合适的 渲染组件。注意,如果要支持url后缀,路由正则后面必须加\.(?P\w+)。如果同时多个存在时,自动根据URL后缀来选择渲染器。

  urlpatterns = [
      url(r'test/$', views.RenderTestView.as_view()),
      url(r'test\.(?P\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 JSONRenderer


class CustomPagination(PageNumberPagination):
    pass


class RenderTestSerializer(serializers.ModelSerializer):
    pass


class 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 HTMLFormRenderer


class 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 HTMLFormRenderer


class 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 BrowsableAPIRenderer


class RenderTestView(APIView):
    renderer_classes = [JSONRenderer, BrowsableAPIRenderer, ]

    def get(self, request, *args, **kwargs):
        pass

你可能感兴趣的:(django,api,rest,Django,API)