Django REST Framework完整开发api示例

最近很多H5或者小程序的广告表单提交需求需要做,索性使用Drf完整各个终端服务器数据汇总吧,完整记录下来供后续参考。

前提准备

服务器端

服务端使用uwsgi+nginx发布我们的Django应用,服务端已经部署完成,详细的操作见:**uwsgi配置发布web服务器**

应用程序服务端

我们这里使用的Django版本为最新的3.0.6Django REST Framework版本为:3.11.0。数据库我们采用mysql

创建项目及应用

我们本地使用windows进行开发,pycharm工具与服务器做了同步处理。

pycharm同步设置

img

配置账户

img

映射配置

img

自动上传

img

使用命令行创建项目

1
2
django-admin startproject django_test_project
django-admin startapp app

settings.py设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 所有主机访问
ALLOWED_HOSTS = ['*']
# 将新创建的app应用添加至已安装应用列表
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app.apps.AppConfig',
]
# 使用mysql数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'dbname #你的数据库名称
'USER': 'dbuser你的数据库用户名
'PASSWORD': 'dbpassword密码
'HOST': '', #你的数据库主机,留空默认为localhost
'PORT': '3306', #你的数据库端口
}
}
# 语言和时区
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'

模型类

我们使用django内置的user模型进行扩展,使用我们的api发起某些请求需要传递token

后续如果牵涉到第三方登陆我们可以保存至第三方用户表,与内置用户表进行一对一关联。

用户提交的表单我们也要单独存放一个表(用户表单我们不确定都有什么字段,所以我们将用户表单提交过来的数据打包为json字符串进行保存),暂时创建以上三张表,后续根据需要我们慢慢添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.db import models
from django.contrib.auth.models import User
# Create your models here.

# 用户token
class UserToken(models.Model):
user = models.OneToOneField(to=User, verbose_name='用户', on_delete=models.CASCADE)
token = models.UUIDField(verbose_name='token')
uptime = models.DateTimeField(auto_now=True, verbose_name='更新时间')

# 第三方用户登录信息
class ThirdParty(models.Model):
user = models.OneToOneField(to=User, verbose_name='用户', on_delete=models.CASCADE)
wechat = models.CharField(max_length=50, verbose_name='微信唯一id', null=True)
alipay = models.CharField(max_length=50, verbose_name='支付宝唯一id', null=True)
thumbnail = models.ImageField(upload_to='thumbnail', verbose_name='图片', null=True)

# 表单数据
class FormData(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(to=User, verbose_name='用户', on_delete=models.CASCADE)
# 用来标识数据来源于该用户的哪个表单
slug = models.SlugField(max_length=100, verbose_name='来源')
body = models.CharField(max_length=255, verbose_name='表单json数据')
uptime = models.DateTimeField(auto_now=True, verbose_name='更新时间')

迁移数据

迁移之前尝试运行了下该程序,直接报错了:

1
django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.13 or newer is required, you have 0.9.2

嫌弃我们版本低?

项目的__init__.py文件中修改

1
2
3
import pymysql
pymysql.version_info = (1, 3, 13, "final", 0)
pymysql.install_as_MySQLdb()

开始迁移

1
2
python manage.py makemigrations
python manage.py migrati

顺手创建个管理员吧

1
python manage.py createsuperuser

创建序列化

app应用中我们创建untils文件夹,用来存放后续认证、频率、权限、序列化等文件。我们现在该文件夹中创建serializers.py用来存放自定义序列化

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-
# @Time : 2020/5/12 13:33
# @Author : Tony Yu
# @Author URI: https://www.diandian100.cn
from rest_framework import serializers

from app.models import FormData

class FormDataSerializer(serializers.ModelSerializer):
class Meta:
model = FormData
fields = '__all__'

我们先做一个表单数据表序列化,且并没有对任何字段做验证,因为使用的ModelSerializer,所以默认会验证模型类中没有定义null=True的字段。

使用token后续路由验证

牵涉到表单提交数据,我们要做token验证,此处我们直接使用jwt生成token进行后续验证使用。关于jwt的详细使用请参见:xxxxxx

token生成和验证

此处我们直接使用app/untils/jwt_tokens.py来存放上述代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# -*- coding: utf-8 -*-
# @Time : 2020/5/13 11:43
# @Author : Tony Yu
# @Author URI: https://www.diandian100.cn
import datetime

import jwt
from jwt import exceptions




salt = 'iv%x1fo9l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv='

def generate_token(playload, timeout = 120):
'''
生成token
:param playload: 一般为用户id、用户名的字典
:param timeout: 多久过期,单位:分钟
:return:
'''
# 构造header
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
# 构造payload
playload = playload
# token有效期,我这边设置为了分钟,即x分钟后过期
playload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)
# 构造signature即token
token = jwt.encode(payload=playload, key=salt, algorithm="HS256", headers=headers).decode('utf-8')
return token

def validate_token(token):
'''校验token有效性'''
# 定义返回相应字典
result = {'status': False, 'data': None, 'error': None}
try:
# 从token中获取payload【不校验合法性】
# unverified_payload = jwt.decode(token, None, False)
# print(unverified_payload)
# 从token中获取payload【校验合法性】;如果token正确,返回生成token时的playload字典
verified_payload = jwt.decode(token, salt, True)

result['status'] = True
result['data'] = verified_payload
except exceptions.ExpiredSignatureError:
result['error'] = 'token已失效'
except jwt.DecodeError as err:
result['error'] = 'token认证失败'
except jwt.InvalidTokenError:
result['error'] = '非法的token'
return result

登录视图

要想提交数据,要携带有效token,初次token需要使用用户名和密码进行交换,获取token我们就放在这个登录视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import datetime

from django.contrib import auth

from rest_framework.views import APIView
from rest_framework.response import Response

from app.untils.response import ResponseDict
from app.untils import jwt_tokens


# 登录
class Login(APIView):
def post(self, request):
# 实例化自定义响应器,响应给前端的字典
res = ResponseDict()
# 数据接收字典
recev_data = {'username', 'password'}
# 判断定义的待接收字典是否是用户提交的post数据的子集,即用户提交的数据是否包含了用户名和密码
if recev_data.issubset(set(request.data)):
# 接收用户提交的用户名密码
user_info = dict()
for item in recev_data:
user_info[item] = request.data.get(item)
# 验证该用户
user_obj = auth.authenticate(**user_info)
# 判断用户是否存在
if user_obj:
# 生成token,并将token有效期设为了10分钟
token = jwt_tokens.generate_token({"id":user_obj.id, "name":user_obj.username}, 10)
# 封装返回数据
res.data = {
"access_token": token
}
res.msg = "登录成功"
res.expires = 600
else:
res.code = 1001
res.msg = "用户名或密码错误"
else:
res.code = 1001
res.msg = "参数有误,请重新提交"
return Response(res.dict)

我们测试一下:

尝试字段携带错误,请求一下:

img

尝试账户密码错误,请求一下:

img

最后使用正确的账户信息请求一下:

可以看到系统成功给我们返回了token

img

自定义认证组件

上面我们定义了token生成和验证token的方法,现在我们要自定义一个认证类。

在此之前我们在上面使用了一个自定义响应体类,没有放出来直接使用了,这里我们放出一下app\untils\response.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding: utf-8 -*-
# @Time : 2020/5/12 16:26
# @Author : Tony Yu
# @Author URI: https://www.diandian100.cn
class ResponseDict:
'''定义一个自定义响应字典'''
def __init__(self):
self.code = 1000
self.data = None
self.msg = ''
# self.expires = 7200
def dict(self):
return self.__dict__

if __name__ == '__main__':
res = ResponseDict()
res.msg = '测试上 '
print(res.dict)

接下来是我们的自定义认证类app\untils\authentications.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# -*- coding: utf-8 -*-
# @Time : 2020/5/13 14:09
# @Author : Tony Yu
# @Author URI: https://www.diandian100.cn
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app.untils import jwt_tokens
from app.untils.response import ResponseDict

class JwtTokenAuthentication(BaseAuthentication):
def authenticate_header(self, request):
pass
def authenticate(self, request):
# 实例化我们的自定义响应体类
res = ResponseDict()
# 获取用户请求头中携带的token
authorization = request.META.get('HTTP_AUTHORIZATION', '')
# 判断是否携带token
if not authorization:
res.msg = '未获取到Authorization请求头'
res.code = 1001
raise AuthenticationFailed(res.dict)
# 验证用户携带的token
result = jwt_tokens.validate_token(authorization)
# 判断token是否有效,无效输出错误提示
if not result['status']:
res.msg = result['error']
res.code = 1001
raise AuthenticationFailed(res.dict)
# token有效,返回用户字典和token
return (result, authorization)

定义表单视图类

我们的表单暂时只定义列表页和post提交页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from rest_framework.views import APIView
from rest_framework.response import Response

from app.models import FormData as FormDataModel
from app.untils.serializers import FormDataSerializer
from app.untils.response import ResponseDict
from app.untils.authentications import JwtTokenAuthentication

class FormDatas(APIView):
# token验证类列表
authentication_classes = [JwtTokenAuthentication, ]
def get(self, request):
res = ResponseDict()
# 获取所有formdatas对象
form_datas = FormDataModel.objects.all()
# 使用序列化类序列化formsdatas字段,many代表获取的是多条信息
serialize_data = FormDataSerializer(form_datas, many=True)
res.msg = "成功"
res.data = serialize_data.data
return Responseres.dict)
def post(self, request):
res = ResponseDict()
#使用序列化模型接收用户提交的数据
serialize_data = FormDataSerializer(data=request.data)
if serialize_data.is_valid():
# 保存表单数据
serialize_data.save()
res.msg = "提交成功"
res.data = serialize_data.data
else:
res.msg = "提交失败"
res.data = serialize_data.errors
return Response(res.dict)

测试:

img

token已经过期了,我们需要重新获取token,我们输入重新获取的token再次尝试:

img

数据正确返回,接下来我们测试下post数据

img

我们故意不填写body字段进行数据提交,出现以上提示,再次尝试一个正确的

img

这次数据成功提交了。

自定义节流

自定义限制频率类路径:app\untils\visit_thottle.py

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
# @Time : 2020/5/13 17:07
# @Author : Tony Yu
# @Author URI: https://www.diandian100.cn
from rest_framework.throttling import SimpleRateThrottle

class VisitThottle(SimpleRateThrottle):
# 设定访问速度
rate = '5/m'
def get_cache_key(self, request, view):
return self.get_ident(request)

尝试刷新5次再次访问如下:

img

最后说下,我们api应用肯定会有跨域问题,我这里安装了第三方跨域插件corsheaders,使用pip安装好该插件在settings里做以下设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'app.apps.AppConfig',
# 解决跨域
'corsheaders'
]
MIDDLEWARE = [
# 解决跨域
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
CORS_ORIGIN_ALLOW_ALL = True

至此,我们的api已经基本完成了,其他视图无非重复以上的动作,这里我们没有做字段验证,使用了数据库模型默认验证,另外我们没有添加权限验证、分页等,因为我们的业务还是很简单的,需要用到的可以参考我之前的文章drf各个组件使用流程