Django & DRF

概述:Django & DRF

[TOC]

1. URL & PATH (diff)

path 与 url 是两个不同的模块,效果都是响应返回页面,path调用的是python第三方模块或框架,而url则是自定义的模块,如Views下的def函数对应url中的参数值。

1
2
3
4
url(r'^login',views.login),

def login(request):
return render(request,'login.html')

主要是版本问题:1.x版本用URL,2.x版本用path

在settings.py中有一个ROOT_URLCONF设置,通过对应url文件匹配请求网址

2. Settings设置redis

1
2
3
4
5
redis://用户名!密码@地址:端口/通道

redis://:django!root2019@10.101.203.209:6379/9

redis://9.134.104.42:6379

3. 导入导出DB

1.导出数据库到JSON文件

1
python manage.py dumpdata > db_bak.json

2.JSON文件导入到数据库

1
python manage.py loaddata db.json

4. @cached_property 缓存装饰器

cached_property主要实现的功能:被修饰的的方法在第一次计算后将方法作为属性存入对象的__dict__[‘方法名’],下次读值时直接从__dict__['getWorkYear']取结果(eg. ),避免了多次计算。
使用限制:只能用于只带默认参数的类

另外,类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类__dict__

对象的__dict__中存储了一些self.xxx

不使用@cached_property

1
2
3
4
5
6
7
8
9
10
class User(object):
def __init__(self, age=0):
self.age=age
def getWorkYear(self):
return 65-self.age

user=User(20)
print(user.getWorkYear)#<bound method User.getWorkYear of <__main__.User object at 0A..88>>
print(user.getWorkYear()) #45
print(user.__dict__) #{'age': 20}

使用@cached_property

1
2
3
4
5
6
7
8
9
10
11
12
from django.utils.functional import cached_property
class User(object):
def __init__(self, age=0):
self.age=age
@cached_property
def getWorkYear(self):
return 65-self.age

user=User(20)
print(user.getWorkYear) #45
print(user.getWorkYear()) #error
print(user.__dict__) #{'age': 20, 'getWorkYear': 45}

5. select_for_update

  1. 该语句必须在事务内使用:with transaction.atomic()
  2. 返回一个锁住行直到事务结束的查询集,如果数据库支持,它将生成一个 SELECT ... FOR UPDATE 语句,所有匹配的行将被锁定,直到事务结束。这意味着可以通过锁防止数据被其它事务修改。
  3. 如果其他事务锁定了相关行,那么本查询将被阻塞,直到锁被释放。 如果这不想要使查询阻塞的话,使用select_for_update(nowait=True)。 如果其它事务持有冲突的锁,互斥锁, 那么查询将引发 DatabaseError 异常。
  4. 可以使用select_for_update(skip_locked=True)忽略锁定的行,nowait和skip_locked是互斥的,同时设置会导致ValueError。
  5. postgresql,oracle和mysql数据库后端支持select_for_update(),MySQL不支持nowait和skip_locked参数。
  6. 使用不支持这些选项的数据库后端(如MySQL)将nowait=True或skip_locked=True转换为select_for_update()将导致抛出DatabaseError异常,这可以防止代码意外终止。
1
2
3
4
5
with transaction.atomic():
try:
p = Person.objects.select_for_update().get(id=pid) # 添加行锁
except Person.DoesExist:
return None

6. generics.ListAPIView 关键字搜索

generics.ListAPIView.filter_backends/search_fields 提供了关键字搜索功能

当view中设置了search_fields属性时,才应用SearchFilter类,search_fields属性应该是model中文本类型字段的名称列表,例如CharFieldTextField

1
2
3
4
5
6
from rest_framework.filters import SearchFilter

class UserListView(generics.ListAPIView):
serializer_class = UserSerializer
filter_backends = [SearchFilter]
search_fields = ['name', 'desc']

7. @action

method表示请求方法

detail表示该action路径是否与单一资源对应

1
2
3
4
5
6
7
detail=True ====> abc/pk/action
detail=False ====> abc/action

@action(methods=["POST"], detail=True)
def function_name(self, *args, **kwargs):
instance = self.get_object()
# ...

8. settings

  • BASE_DIR: 指向Django项目目录的绝对路径
  • 国际化配置 & 地区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Etc/GMT-8'

USE_I18N = True

USE_L10N = USE_TZ = False

LOCALE_PATHS = (
os.path.join(SITE_DIR, 'locale'),
os.path.join(SITE_DIR, 'locale', 'rest_framework'),
)

# 日期 / 时间格式
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'

DATE_FORMAT = '%Y-%m-%d'

TIME_FORMAT = '%H:%M:%S'

9. 国际化

1
2
3
# 使用国际化的代码
from django.utils.translation import gettext as _
message=_('App name must be unique')
1
2
3
4
5
# settings.py
LOCALE_PATHS = (
os.path.join(SITE_DIR, 'locale'),
os.path.join(SITE_DIR, 'locale', 'rest_framework'),
)
1
2
3
4
5
# locale/zh_Hans_LC_MESSAGES/django.po (翻译文件)

#: cd/apps/app/serializers.py:19
msgid "App name must be unique"
msgstr "应用名称必须唯一"
1
2
3
4
5
# shell命令
# 修改文本后执行
# 编译执行
python manage.py makemessages
python manage.py compilemessages

10. ORM

value 含义 eg
__gt >
__gte >=
__lt <
__lte <=
__exact 精确等于
__iexact 忽略大小写精确等于
__contains 包含 like ‘%aaa%’
__icontains 忽略大小写包含 ilike ‘%aaa%’
__in 属于 name__in=[10,20]
__isnull 判空
__startswith 开头
__istartswith 忽略大小写开头
__endswith 结尾
__iendswith 忽略大小写结尾
__range 范围
__year
__month
__day

多表操作:

1
2
3
4
5
6
class A(models.Model):
name = models.CharField(u'名称')
class B(models.Model):
aa = models.ForeignKey(A)

B.objects.filter(aa__name__contains='searchtitle')#查询B表中外键aa所对应的表中字段name包含searchtitle的B表对象。

返回QuerySets的API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
方法名	                	解释
filter() 过滤查询对象。
exclude() 排除满足条件的对象
annotate() 使用聚合函数
order_by() 对查询集进行排序
reverse() 反向排序
distinct() 对查询集去重
values() 返回包含对象具体值的字典的QuerySet
values_list() 与values()类似,只是返回的是元组而不是字典。
dates() 根据日期获取查询集
datetimes() 根据时间获取查询集
none() 创建空的查询集
all() 获取所有的对象
union() 并集
intersection() 交集
difference() 差集
select_related() 附带查询关联对象
prefetch_related() 预先查询
extra() 附加SQL查询
defer() 不加载指定字段
only() 只加载指定的字段
using() 选择数据库
select_for_update() 锁住选择的对象,直到事务结束。
raw() 接收一个原始的SQL查询

annotate:使用提供的聚合表达式查询对象

1
2
3
4
5
6
7
8
9
from django.db.models import Count
# 如果正在操作一个Blog列表,你可能想知道每个Blog有多少Entry
q = Blog.objects.annotate(Count('entry'))
q[0].name # 'Blogasaurus'
q[0].entry__count # 42

# 分组查询(根据app和name进行分组)
Cluster.enables.values('app', 'name').annotate(repeated_count=Count('name'))\
.order_by('repeated_count').filter(repeated_count__gt=1).values('app', 'name', 'repeated_count')

CURD 示例

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# insert
Model.objects.create()

# select
Model.objects.all()
Model.objects.get(id=1) # 仅返回一个
Model.objects.filter(name='junming', price>1000) # 返回列表
Model.objects.filter(name='junming', price>1000).first()/last() # 返回列表第一个/最后一个元素
Model.objects.exclude(name='jimmy') # 过滤
Model.objects.order_by("id") # 以id升序返回列表
Model.objects.order_by("-id") # 以id降序返回列表
Model.objects.order_by("id").reverse() # 以id降序返回列表
Model.objects.count() # 总数量
Model.objects.filter(price__gt=10).exists() # 判断价格大于10的数据是否存在
Model.objects.exclude(price__gte=10) # 排除大于等于10的数据
Model.objects.values('id') # 查看id字段部分数据
Model.objects.distinct() # 去重
Model.objects.filter(title__contains="tencent") # 包含字段
Model.objects.filter(title__icontains="tencent") # 包含字段(不区分大小写)
Model.objects.filter(title__startswith="tencent") # 包含字段(开头)
Model.objects.filter(title__endswith="tencent") # 包含字段(开头)
Model.objects.get(id=1944).__dict__.keys() # 仅查看字段名
Model.objects.filter(pub_date__month=10) # 10月
Model.objects.filter(created_at__range=(start_date, end_date)) # 范围

# update
model.update(name='guoguo')

# drop
model.delete()

# save
model.save()

# values_list 获取元组形式结果(仅获得name和qq字段)
authors = Author.objects.values_list('name', 'qq')

# 如果只需要 1 个字段,可以指定 flat=True,当只传递单字段时,可以传递flat参数,当设置为True时,返回的结果将是单值而不是元组。
Author.objects.values_list('name', flat=True)

# values 获取字典形式的结果
Author.objects.values('name', 'qq')

# 输出SQL细节,设置LOGGING.loggers.django.db.backends->level:DEBUG
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG' if DEBUG else 'INFO',
},
},
}

# 查看 Django queryset 执行的 SQL
print str(Release.objects.all().query)

# 基于双下划线的跨表查询
#正向:属性名称__跨表的属性名称 反向:小写类名__跨表的属性名称
#一对多(publish是Book的外键)
Book.objects.filter(publish__name="ten").values_list("title", "price")

# F对象:可用于查询属性之间比较
# eg.bread值大于comment值
BookInfo.objects.filter(bread__gt=F('comment'))

# Q对象:调用(&与)(|或)(~NOT)
Poll.objects.get(
Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

BookInfo.objects.filter(~Q(pk=3))

# 约束条件
constraints = [models.UniqueConstraint(fields=['app', 'name'], condition=models.Q(is_valid=True), name='unique_app_artifact_name')]

# 日期查询
BookInfo.objects.filter(public_data__year=2020)
BookInfo.objects.filter(public_data__gt=date(2020,1,1))

# 聚合函数
# 使用aggregate()过滤器,包含 Avg、Count、Max、Min、Sum, aggregate返回值是一个字典类型,eg.{'聚合类小写__属性名': 值}
BookInfo.objects.aggregate(Sum('bread'))
# 另外 count 通常不用aggregate
BookInfo.objects.count()

# 由一到多(常用)
book = BookInfo.objects.get(id=1)
book.peopleinfo_set.all()

11. Models.EnableManager

Enable功能自定义条件过滤数据

1
2
3
4
5
6
7
enables = EnableManager()

class EnableManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_valid=True)

# Person.enables.filter(name__contains='bear')

cookie

1
2
response.set_cookie('number', 123)  # 设置
request.COOKIES['number'] # 获取

session:Django中默认开启session

1
2
3
4
5
6
7
8
9
10
11
# settings.py
MIDDLEWARE = ['django.contrib.sessions.middleware.SessionMiddleware']

# 指定session数据的存储方式
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 默认
SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 缓存,丢失无法找回,比DB方式更快
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # 混合村粗,优先从内存中存取,没有则从数据库中存取

# 存储在数据库中需要在应用中注册session
INSTALLED_APPS = ['django.contrib.sessions'] # 生成一个session表
# 通常包含session_key,session_data,expire_date三个字段

设置/获取/清除

1
2
3
4
5
request.session['key'] = 'hello' # 以键值对的格式写session
request.session.get('key') # 根据键读取值
request.session.clear() # 清除所有session,在存储中删除值部分
del request.session['key'] # 删除session中的指定键及值,在存储中只删除某个键及对 应的值
request.session.set_expiry(value) # 设置会话的超时时间,如果没有指定过期时间则两个星期 后过期,若value=None表示永不过期

13. 授权和认证

认证是通过用户提供的用户ID/密码组合或者Token来验证用户的身份。权限(Permission)的校验发生验证用户身份以后,是由系统根据分配权限确定用户可以访问何种资源以及对这种资源进行何种操作,这个过程也被称为授权(Authorization)。

身份验证是将传入的请求对象(request)与一组标识凭据(例如请求来自的用户或其签名的令牌token)相关联的机制。

用户通过认证后request.user返回Django的User实例,否则返回AnonymousUser的实例。request.auth通常为None。如果使用token认证,request.auth可以包含认证过的token。

Django REST Framework提供了如下几种认证方案:

  • Session认证SessionAuthentication类:此认证方案使用Django的默认session后端进行身份验证。当客户端发送登录请求通过验证后,Django通过session将用户信息存储在服务器中保持用户的请求状态。Session身份验证适用于与你的网站在相同的Session环境中运行的AJAX客户端 (注:这也是Session认证的最大弊端)。
  • 基本认证BasicAuthentication类:此认证方案使用HTTP 基本认证,针对用户的用户名和密码进行认证。使用这种方式后浏览器会跳出登录框让用户输入用户名和密码认证。基本认证通常只适用于测试。
  • 远程认证RemoteUserAuthentication类:此认证方案为用户名不存在的用户自动创建用户实例。这个很少用,具体见文档。
  • Token认证TokenAuthentication类:该认证方案是DRF提供的使用简单的基于Token的HTTP认证方案。当客户端发送登录请求时,服务器便会生成一个Token并将此Token返回给客户端,作为客户端进行请求的一个标识以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。后面我们会详细介绍如何使用这种认证方案。

注意:如果你在生产环境下使用BasicAuthentication和TokenAuthentication认证,你必须确保你的API仅在https可用。

13.1 DRF自带认证

(1)创建超级管理员

会在auth_user内置表中新增一条数据

1
python manage.py createsuperuser --username USERNAME --email EMAIL

(2)添加登录入口路由

该方式前后端不分离,不推荐

1
2
3
4
5
6
7
添加路由from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path(‘admin/’, admin.site.urls),
path(‘api/’, include(‘rest_framework.urls’))
]

(3)对指定API限制读写验证

1
2
3
4
5
6
from rest_framework import permissions

class List(generics.ListCreateAPIView):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly)

(4)常用DRF自带权限类

除了IsAuthenticatedOrReadOnly 类,DRF自带的常用权限类还包括:

  • IsAuthenticated类:仅限已经通过身份验证的用户访问;
  • AllowAny类:允许任何用户访问;
  • IsAdminUser类:仅限管理员访问;
  • DjangoModelPermissions类:只有在用户经过身份验证并分配了相关模型权限时,才会获得授权访问相关模型。
  • DjangoModelPermissionsOrReadOnly类:与前者类似,但可以给匿名用户访问API的可读权限。
  • DjangoObjectPermissions类:只有在用户经过身份验证并分配了相关对象权限时,才会获得授权访问相关对象。通常与django-gaurdian联用实现对象级别的权限控制。

(5)自定义权限类

IsAuthenticatedOrReadOnly 类并不能实现只有文章 article 的创建者才可以更新或删除它,这时我们还需要自定义一个名为IsOwnerOrReadOnly 的权限类,把它加入到ArticleDetail视图里。

首先我们在blog文件夹下创建permissions.py,添加如下代码:

# blog/permissions.py

1
2
3
4
5
6
7
8
9
10
11
12
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
"""
自定义权限只允许对象的创建者才能编辑它。"""
def has_object_permission(self, request, view, obj):
# 读取权限被允许用于任何请求,
# 所以我们始终允许 GET,HEAD 或 OPTIONS 请求。
if request.method in permissions.SAFE_METHODS:
return True
# 写入权限只允许给 article 的作者。
return obj.author == request.user

#blog/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from rest_framework import generics
from rest_framework import permissions
from .permissions import IsOwnerOrReadOnly


class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

# important
def perform_create(self, serializer):
serializer.save(author=self.request.user)


class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class =ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)

(6)设置权限的方式

在前面的案例中,我们都是在基于类的API视图里通过permission_classes属性设置的权限类。如果你有些权限是全局或全站通用的,你还可以在settings.py中使用 DEFAULT_PERMISSION_CLASSES 全局设置默认权限策略。

例如:

1
2
3
4
5
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}

如果未指定,则此设置默认为允许无限制访问:

1
2
3
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
)

基于函数的视图编写API,你可以按如下方式给你的函数视图添加权限:

1
2
3
4
5
6
7
8
9
10
11
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)

(7)总结 DRF 认证方案

方式1:settings.py中设置默认的全局认证方案

1
2
3
4
5
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)}

方式2:基于类的视图(CBV)中使用

1
2
3
4
5
6
7
8
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)

方式3:函数视图中使用

1
2
3
4
5
6
7
8
9
@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` 实例。
'auth': unicode(request.auth), # None
}
return Response(content)

自定义的认证方案,要继承BaseAuthentication类并且重写.authenticate(self, request)方法。如果认证成功,该方法应返回(user, auth)的二元元组,否则返回None

在某些情况下,你可能不想返回None,而是希望从.authenticate()方法抛出AuthenticationFailed异常。

通常你应该采取的方法是:

  • 如果不尝试验证,返回None。还将检查任何其他正在使用的身份验证方案。
  • 如果尝试验证但失败,则抛出AuthenticationFailed异常。无论任何权限检查也不检查任何其他身份验证方案,立即返回错误响应。

你也可以重写.authenticate_header(self, request)方法。如果实现该方法,则应返回一个字符串,该字符串将用作HTTP 401 Unauthorized响应中的WWW-Authenticate头的值。

如果.authenticate_header()方法未被重写,则认证方案将在未验证的请求被拒绝访问时返回HTTP 403 Forbidden响应。

示例

以下示例将以自定义请求标头中名称为’X_USERNAME’提供的用户名作为用户对任何传入请求进行身份验证,其它类似自定义认证需求比如支持用户同时按用户名或email进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions

class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
username = request.META.get('X_USERNAME')
if not username:
return None

try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No such user')

return (user, None)

13.2 Token认证

(1)前后端分离推荐token认证

  • Token无需存储降低服务器成本,session是将用户信息存储在服务器中的,当用户量增大时服务器的压力也会随着增大。
  • 防御CSRF跨站伪造请求攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户信息就容易泄露。
  • 扩展性强,session需要存储无法共享,当搭建了多个服务器时其他服务器无法获取到session中的验证数据用户无法验证成功。而Token可以实现服务器间共享,这样不管哪里都可以访问到。
  • Token可以减轻服务器的压力,减少频繁的查询数据库。
  • 支持跨域访问, 适用于移动平台应用

(2)TokenAuthentication

DRF自带的TokenAuthentication方案可以实现基本的token认证,整个流程如下:

首先,你需要将修改settings.py, 加入如下app。

1
2
3
4
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)

其次,你需要为你的用户生成令牌(token)。如果你希望在创建用户时自动生成token,你可以借助Django的信号(signals)实现,如下所示:

1
2
3
4
5
6
7
8
9
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)

如果你已经创建了一些用户,则可以打开shell为所有现有用户生成令牌,如下所示:

1
2
3
4
5
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token

for user in User.objects.all():
Token.objects.get_or_create(user=user)

你还可以在admin.py中给用户创建token,如下所示:

1
2
from rest_framework.authtoken.admin import TokenAdmin
TokenAdmin.raw_id_fields = ['user']

从3.6.4起,你还可以使用如下命令为一个指定用户新建或重置token。

1
2
./manage.py drf_create_token <username> # 新建
./manage.py drf_create_token -r <username> # 重置

接下来,你需要暴露用户获取token的url地址(API端点).

1
2
3
from rest_framework.authtoken import views
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token)]

这样每当用户使用form表单或JSON将有效的usernamepassword字段POST提交到以上视图时,obtain_auth_token视图将返回如下JSON响应:

1
{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }

客户端拿到token后可以将其存储到本地cookie或localstorage里,下次发送请求时把token包含在Authorization`` HTTP头即可,如下所示:

1
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

你还可以通过curl工具来进行简单测试。

1
curl -X GET http://127.0.0.1:8000/api/example/ -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'

默认的obtain_auth_token视图返回的json响应数据是非常简单的,只有token一项。如果你希望返回更多信息,比如用户id或email,就就要通过继承ObtainAuthToken类量身定制这个视图,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response

class CustomAuthToken(ObtainAuthToken):
def post(self, request,*args,**kwargs):
serializer =self.serializer_class(data=request.data,
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created =Token.objects.get_or_create(user=user)
returnResponse({
'token': token.key,
'user_id': user.pk,
'email': user.email
})

然后修改urls.py:

1
2
urlpatterns +=[
path('api-token-auth/',CustomAuthToken.as_view())]

最后一步,DRF的TokenAuthentication类会从请求头中获取Token,验证其有效性。如果token有效,返回request.user。至此,整个token的签发和验证就完成了。

13.3 JWT认证

(1)Json Web Token及工作原理

JSON Web Token(JWT)是一种开放标准,它定义了一种紧凑且自包含的方式,用于各方之间安全地将信息以JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。JWT用于为应用程序创建访问token,通常适用于API身份验证和服务器到服务器的授权。那么如何理解紧凑和自包含这两个词的含义呢?

  • 紧凑:就是说这个数据量比较少,可以通过url参数,http请求提交的数据以及http header多种方式来传递。
  • 自包含:这个字符串可以包含很多信息,比如用户id,用户名,订单号id等,如果其他人拿到该信息,就可以拿到关键业务信息。

那么JWT认证是如何工作的呢? 首先客户端提交用户登录信息验证身份通过后,服务器生成一个用于证明用户身份的令牌(token),也就是一个加密后的长字符串,并将其发送给客户端。在后续请求中,客户端以各种方式(比如通过url参数或者请求头)将这个令牌发送回服务器,服务器就知道请求来自哪个特定身份的用户了。

图片

JSON Web Token由三部分组成,这些部分由点(.)分隔,分别是header(头部),payload(有效负载)和signature(签名)。

  • header(头部): 识别以何种算法来生成签名;
  • pyload(有效负载): 用来存放实际需要传递的数据;
  • signature(签名): 安全验证token有效性,防止数据被篡改。

通过http传输的数据实际上是加密后的JWT,它是由两个点分割的base64-URL长字符串组成,解密后我们可以得到header, payload和signature三部分。我们可以简单的使用 https://jwt.io/ 官网来生成或解析一个JWT,如下所示:

图片

(2)Django中如何使用JWT认证

django-rest-framework-simplejwt为Django REST框架提供了JSON Web令牌认证后端。它提供一组保守的默认功能来涵盖了JWT的最常见用例。它还非常容易扩展。

使用pip安装

1
pip install djangorestframework-simplejwt

其次需要告诉DRF我们使用jwt认证作为后台认证方案。修改myproject/settings.py:

#myproject/settings.py

1
2
3
4
5
6
7
8
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}

接下来需要提供用户可以获取和刷新token的urls地址,这两个urls分别对应TokenObtainPairView和TokenRefreshView两个视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.contrib import admin
from django.urls import path, include
from reviews.views import ProductViewSet, ImageViewSet
from rest_framework.routers import DefaultRouter
from django.conf import settings
from django.conf.urls.static import static
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)

router = DefaultRouter()
router.register(r'product', ProductViewSet, basename='Product')
router.register(r'image', ImageViewSet, basename='Image')

urlpatterns = [
path('admin/', admin.site.urls),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('', include(router.urls)),
]

if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

最后可以开始使用postman测试了。通过POST方法发送登录请求到/token/, 请求数据包括username和password。如果登录成功,你将得到两个长字符串,一个是access token(访问令牌),还有一个是refresh token(刷新令牌)

假如你有一个受保护的视图(比如这里的/image/),权限(permission_classes)是IsAuthenticated,只有验证用户才可访问。访问这个保护视图时你只需要在请求头的Authorization选项里输入你刚才获取的access token即可

不给这个access token默认只有5分钟有效。5分钟过后,当你再次访问保护视图时,你将得到如下token已失效或过期的提示

那么问题来了,Simple JWT中的access token默认有效期是5分钟,那么refresh token默认有效期是多长呢? 答案是24小时。

(3)更改Simple JWT的默认设置

Simple JWT的默认设置如下所示:

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

DEFAULTS = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,

'ALGORITHM': 'HS256',
'SIGNING_KEY': settings.SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,

'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',

'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',

'JTI_CLAIM': 'jti',

'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

如果要覆盖Simple JWT的默认设置,可以修改settings.py, 如下所示。下例将refresh token的有效期改为了15天。

1
2
3
4
5
6
from datetime import timedelta

SIMPLE_JWT = {
'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
'ROTATE_REFRESH_TOKENS': True,
}

(4)自定义令牌(token)

如果你对Simple JWT返回的access token进行解码,你会发现这个token的payload数据部分包括token类型,token失效时间,jti(一个类似随机字符串)和user_id。如果你希望在payload部分提供更多信息,比如用户的username,这时你就要自定义令牌(token)了。

编写你的myapp/seralizers.py,添加如下代码。该序列化器继承了TokenObtainPairSerializer类。

1
2
3
4
5
6
7
8
9
10
11

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super(MyTokenObtainPairSerializer, cls).get_token(user)

# Add custom claims
token['username'] = user.username
return token

不使用Simple JWT提供的默认视图,使用自定义视图。修改myapp/views.py, 添加如下代码:

1
2
3
4
5
6
7
8
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework.permissions import AllowAny
from .serializers import MyTokenObtainPairSerializer


class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (AllowAny,)
serializer_class = MyTokenObtainPairSerializer

修改myproject/urls.py, 添加如下代码,将/token/指向新的自定义的视图。注意:本例中的app名为reviews,所以是从reviews.views导入的MyObtainTokenPairView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.contrib import admin
from django.urls import path, include
from reviews.views import ProductViewSet, ImageViewSet, MyObtainTokenPairView
from rest_framework.routers import DefaultRouter
from django.conf import settings
from django.conf.urls.static import static
from rest_framework_simplejwt.views import TokenRefreshView


router = DefaultRouter()
router.register(r'product', ProductViewSet, basename='Product')
router.register(r'image', ImageViewSet, basename='Image')

urlpatterns = [
path('admin/', admin.site.urls),
path('token/', MyObtainTokenPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('', include(router.urls)),
]

if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

重新发送POST请求到/token/,你将获得新的access token和refresh token

对重新获取的access token进行解码,你将看到payload部分多了username的内容,是不是很酷? 在实际API开发过程中,通过Json Web Token传递更多数据非常有用。

(5)自定义认证后台(Backend)

上面的演示案例是通过用户名和密码登录的,如果我们希望后台同时支持邮箱/密码,手机/密码组合登录怎么办? 这时我们还需要自定义认证后台(Backend)。

修改users/views.py, 添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from django.contrib.auth import get_user_model

User = get_user_model()

class MyCustomBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(email=username) )
if user.check_password(password):
return user
except Exception as e:
return None

修改myproject/settings.py, 把你自定义的验证方式添加到AUTHENTICATION_BACKENDS里去。

1
2
3
AUTHENTICATION_BACKENDS = (
'users.views.MyCustomBackend',
)

修改好后,你使用postman发送邮箱/密码组合到/token/,将同样可以获得access token和refresh token

14. DRF 分页

Django REST Framework提供3种分页类

  • PageNumberPagination类:简单分页器。支持用户按?page=3这种方式查询,你可以通过page_size这个参数手动指定每页展示给用户数据的数量。它还支持用户按?page=3&size=10这种更灵活的方式进行查询,这样用户不仅可以选择页码,还可以选择每页展示数据的数量。对于第二种情况,你通常还需要设置max_page_size这个参数限制每页展示数据的最大数量,以防止用户进行恶意查询(比如size=10000), 这样一页展示1万条数据将使分页变得没有意义。
  • LimitOffsetPagination类:偏移分页器。支持用户按?limit=20&offset=100这种方式进行查询。offset是查询数据的起始点,limit是每页展示数据的最大条数,类似于page_size。当你使用这个类时,你通常还需要设置max_limit这个参数来限制展示给用户数据的最大数量。
  • CursorPagination类:加密分页器。这是DRF提供的加密分页查询,仅支持用户按响应提供的上一页和下一页链接进行分页查询,每页的页码都是加密的。使用这种方式进行分页需要你的模型有”created”这个字段,否则你要手动指定ordering排序才能进行使用。

(1)使用PageNumberPagination类

DRF中使用默认分页类的最简单方式就是在settings.py中进行全局配置,如下所示:

1
2
3
4
5
REST_FRAMEWORK ={
'DEFAULT_PAGINATION_CLASS':'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE':2

}

但是如果你希望用户按?page=3&size=10这种更灵活的方式进行查询,你就要进行个性化定制。在实际开发过程中,定制比使用默认的分页类更常见,具体做法如下。

第一步: 在app目录下新建pagination.py, 添加如下代码:

#blog/pagination.py

1
2
3
4
5
6
from rest_framework.pagination import PageNumberPagination

class MyPageNumberPagination(PageNumberPagination):
page_size = 2 # default page size
page_size_query_param = 'size' # ?page=xx&size=??
max_page_size = 10 # max page size

我们自定义了一个MyPageNumberPagination类,该类继承了PageNumberPagination类。我们通过page_size设置了每页默认展示数据的条数,通过page_size_query_param设置了每页size的参数名以及通过max_page_size设置了每个可以展示的最大数据条数。

第二步:使用自定义的分页类

在基于类的视图中,你可以使用pagination_class这个属性使用自定义的分页类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from rest_framework import viewsets
from .pagination import MyPageNumberPagination


class ArticleViewSet(viewsets.ModelViewSet):
# 用一个视图集替代ArticleList和ArticleDetail两个视图
queryset = Article.objects.all()
serializer_class = ArticleSerializer
pagination_class = MyPageNumberPagination


# 自行添加,将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

# 自行添加,将request.user与author绑定
def perform_update(self, serializer):
serializer.save(author=self.request.user)

当然定制分页类不限于指定page_size和max_page_size这些属性,你还可以改变响应数据的输出格式。比如我们这里希望把next和previous放在一个名为links的key里,我们可以修改MyPageNumberPagination类,重写get_paginated_response方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response


class MyPageNumberPagination(PageNumberPagination):
page_size = 2 # default page size
page_size_query_param = 'size' # ?page=xx&size=??
max_page_size = 10 # max page size
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'results': data
})

注意:重写get_paginated_response方法非常有用,你还可以给分页响应数据传递额外的内容,比如code状态码等等。

前面的例子中我们只在单个基于类的视图或视图集中使用到了分页类,你还可以修改settings.py全局使用你自定义的分页类,如下所示。展示效果是一样的,我们就不详细演示了。

1
2
3
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'blog.pagination.MyPageNumberPagination',
}

(2)使用LimitOffsetPagination类

使用这个分页类最简单的方式就是在settings.py中进行全局配置,如下所示:

1
2
3
REST_FRAMEWORK = {    
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
}

展示效果如下所示,从第6条数据查起,每页展示2条。

你也可以自定义MyLimitOffsetPagination类,在单个视图或视图集中使用,或者全局使用。

1
2
3
4
5
6
7
from rest_framework.pagination import LimitOffsetPagination

class MyLimitOffsetPagination(LimitOffsetPagination):
default_limit = 5 # default limit per age
limit_query_param = 'limit' # default is limit
offset_query_param = 'offset' # default param is offset
max_limit = 10 # max limit per age

(3)使用CursorPagination类

全局使用

1
2
3
4
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 2
}

使用CursorPagination类需要你的模型里有created这个字段,否则你需要手动指定ordering字段。这是因为CursorPagination类只能对排过序的查询集进行分页展示。我们的Article模型只有create_date字段,没有created这个字段,所以会报错。

为了解决这个问题,我们需要自定义一个MyCursorPagination类,手动指定按create_date排序, 如下所示:

#blog/pagination.py

1
2
3
4
5
6
7
from rest_framework.pagination import CursorPagination

class MyArticleCursorPagination(CursorPagination):
page_size = 3 # Default number of records per age
page_size_query_param = 'page_size'
cursor_query_param = 'cursor' # Default is cursor
ordering = '-create_date'

修改settings.py, 使用自己定义的分页类。

1
2
3
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'blog.pagination.MyArticleCursorPagination',
}

响应效果如下所示,你将得到previous和next分页链接。页码都加密了, 链接里不再显示页码号码。默认每页展示3条记录, 如果使用?page_size=2进行查询,每页你将得到两条记录。

当然由于这个ordering字段与模型相关,我们并不推荐全局使用自定义的CursorPagination类,更好的方式是在GenericsAPIView或视图集viewsets中通过pagination_class属性指定,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from rest_framework import viewsets
from .pagination import MyArticleCursorPagination


class ArticleViewSet(viewsets.ModelViewSet):
# 用一个视图集替代ArticleList和ArticleDetail两个视图
queryset = Article.objects.all()
serializer_class = ArticleSerializer
pagination_class = MyArticleCursorPagination
# 自行添加,将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

# 自行添加,将request.user与author绑定
def perform_update(self, serializer):
serializer.save(author=self.request.user)

(4)函数类视图中使用分页类

注意pagination_class属性仅支持在genericsAPIView和视图集viewset中配置使用。如果你使用函数或简单的APIView开发API视图,那么你需要对你的数据进行手动分页,一个具体使用例子如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework.pagination import PageNumberPagination
class ArticleList0(APIView):
"""
List all articles, or create a new article.
"""
def get(self, request, format=None):
articles = Article.objects.all()

page = PageNumberPagination() # 产生一个分页器对象
page.page_size = 3 # 默认每页显示的多少条记录
page.page_query_param = 'page' # 默认查询参数名为 page
page.page_size_query_param = 'size' # 前台控制每页显示的最大条数
page.max_page_size = 10 # 后台控制显示的最大记录条数,防止用户输入的查询条数过大
ret = page.paginate_queryset(articles, request)
serializer = ArticleSerializer(ret, many=True)

return Response(serializer.data)

15. 序列化器 serializer

15.1 基础知识

  • 序列化及反序列化

序列化:将模型类对象转换为字典或者json数据的过程

反序列化:将前端传递的数据保存到模型对象中的过程

  • read-only fields,客户端是不能够通过POST或PUT请求提交相关数据进行反序列化的

  • 当校验通过后,可使用serializer.save()进行数据保存,保存时同时调用create()(通常会报错),需要对create()重写,需要传入一个行参validated_data,它是校验之后的字典数据,其中**validated_data是对该字典进行拆包,同理,更新会同时调用update()

  • 构造
1
Serializer(instance=None, data=empty, **kwarg)

说明:

(1)用于序列化时,将模型类对象传入instance参数

(2)用于反序列化时,将要被反序列化的数据传入data参数

(3)除了instance和data参数外,在构造Serializer对象时,还可通过context参数额外添加数据,通过context参数附加的数据,可以通过Serializer对象的context属性获取。

1
serializer = AccountSerializer(account, context={'request': request})
  • many参数
  1. 序列化器级别:

    若被序列化的包含多条数据的查询集QuerySet,可以通过添加many=True参数补充说明

1
2
3
book_qs = BookInfo.objects.all()
serializer = BookInfoSerializer(book_qs, many=True)
serializer.data
  1. 字段级别:

    如果关联的对象数据不是只有一个,而是包含多个数据,如想序列化图书BookInfo数据,每个BookInfo对象关联的英雄HeroInfo对象可能有多个,此时关联字段类型的指明仍可使用上述几种方式,只是在声明关联字段时,多补充一个many=True参数即可。

1
heroinfo_set = serializers.PrimaryKeyRelatedField(read_only=True, many=True) 

15.2 定义 - 常见序列化方法

(1)指定source来源

1
2
3
4
5
6
7
8
9
10
11
12
13
class ArticleSerializer(serializers.ModelSerializer):
author = serializers.ReadOnlyField(source="author.username")
fstatus = serializers.ReadOnlyField(source="get_status_display")

class Meta:
model = Article
fields = '__all__'
read_only_fields = ('id', 'author', 'create_date')
# 其它字段说明
# db_table:指定自定义数据库表名
# db_tablespace:数据库表空间,eg.Oracle
# ordering:Django模型对象返回的记录结果集按照哪一字段进行排序。
# unique_together:需要通过两个字段保持唯一性时使用。

(2)SerializerMethodField自定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ArticleSerializer(serializers.ModelSerializer):
author = serializers.ReadOnlyField(source="author.username")
status = serializers.ReadOnlyField(source="get_status_display")
cn_status = serializers.SerializerMethodField()

class Meta:
model = Article
fields = '__all__'
read_only_fields = ('id', 'author', 'create_date')

def get_cn_status(self, obj):
if obj.status == 'p':
return "已发表"
elif obj.status == 'd':
return "草稿"
else:
return ''

(3)使用嵌套序列化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class UserSerializer(serializers.ModelSerializer):

class Meta:
model = User
fields = ('id', 'username', 'email')

class ArticleSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True) # 设置required=False表示可以接受匿名用户
status = serializers.ReadOnlyField(source="get_status_display")
cn_status = serializers.SerializerMethodField()

class Meta:
model = Article
fields = '__all__'
read_only_fields = ('id', 'author', 'create_date')

def get_cn_status(self, obj):
if obj.status == 'p':
return "已发表"
elif obj.status == 'd':
return "草稿"
else:
return ''

(4)设置关联模型的深度depth

可代替嵌套序列化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ArticleSerializer(serializers.ModelSerializer):
# author = UserSerializer(read_only=True)
status = serializers.ReadOnlyField(source="get_status_display")
cn_status = serializers.SerializerMethodField()

class Meta:
model = Article
fields = '__all__'
read_only_fields = ('id', 'author', 'create_date')
depth = 1
def get_cn_status(self, obj):
if obj.status == 'p':
return "已发表"
elif obj.status == 'd':
return "草稿"
else:
return ''

15.3 数据验证 (Validation)

在尝试访问经过验证的数据或保存对象实例之前,反序列化进行数据校验需要调用 is_valid()方法,serializer.errors 属性将包含表示校验错误的信息,serializer.validated_data是校验成功的信息:

1
2
3
4
5
serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'email': [u'Enter a valid e-mail address.'], 'created': [u'This field is required.']}

字典中的每个键都是字段名称,值是与该字段对应的任何错误消息的字符串列表。non_field_errors 键也可能存在,并将列出任何常规验证错误。可以使用 REST framework 设置中的 NON_FIELD_ERRORS_KEY 来自定义 non_field_errors 键的名称。

(1)引发无效数据的异常

引发无效数据的异常 (Raising an exception on invalid data)

.is_valid() 方法使用可选的 raise_exception 标志,如果存在验证错误,将会抛出 serializers.ValidationError 异常。

这些异常由 REST framework 提供的默认异常处理程序自动处理,默认情况下将返回 HTTP 400 Bad Request 响应。

1
2
# Return a 400 response if the data was invalid.
serializer.is_valid(raise_exception=True)

(2)字段级别验证 validate_field

字段级别验证 (Field-level validation)

您可以通过向您的 Serializer 子类中添加 .validate_<field_name> 方法来指定自定义字段级的验证。这些类似于 Django 表单中的 .clean_<field_name> 方法。这些方法采用单个参数,即需要验证的字段值。

validate_<field_name> 方法应该返回已验证的值或抛出 serializers.ValidationError 异常。例如:

1
2
3
4
5
6
7
8
9
10
11
12
from rest_framework import serializers

class ArticleSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)

def validate_title(self, value):
"""
Check that the article is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Article is not about Django")
return value

(3)对象级别验证 validate

对象级别验证 (Object-level validation)

要执行需要访问多个字段的任何其他验证,请添加名为 .validate() 的方法到您的 Serializer 子类中。此方法采用单个参数,该参数是字段值的字典。如果需要,它应该抛出 ValidationError 异常,或者只返回经过验证的值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from rest_framework import serializers

class EventSerializer(serializers.Serializer):
description = serializers.CharField(max_length=100)
start = serializers.DateTimeField()
finish = serializers.DateTimeField()

def validate(self, data):
"""
Check that the start is before the stop.
"""
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return data

(4)验证器 (Validators) validate class

序列化器上的各个字段都可以包含验证器,通过在字段实例上声明,例如:

1
2
3
4
5
6
7
def multiple_of_ten(value):
if value % 10 != 0:
raise serializers.ValidationError('Not a multiple of ten')

class GameRecord(serializers.Serializer):
score = IntegerField(validators=[multiple_of_ten])
...

DRF还提供了很多可重用的验证器,比如UniqueValidator,UniqueTogetherValidator等等。通过在内部 Meta 类上声明来包含这些验证器,如下所示。下例中会议房间号和日期的组合必须要是独一无二的。

1
2
3
4
5
6
7
8
9
10
11
class EventSerializer(serializers.Serializer):
name = serializers.CharField()
room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
date = serializers.DateField()

class Meta:
# Each room only has one event per day.
validators = UniqueTogetherValidator(
queryset=Event.objects.all(),
fields=['room_number', 'date']
)

15.4 重写序列化器的create和update方法

假设我们有个Profile模型与User模型是一对一的关系,当用户注册时我们希望把用户提交的数据分别存入User和Profile模型,这时我们就不得不重写序列化器自带的create方法了。

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
class UserSerializer(serializers.ModelSerializer):
profile = ProfileSerializer()

class Meta:
model = User
fields = ('username', 'email', 'profile')

# 通常需要重写create(),同时传入validated_data(是校验之后对数据字典)
# 其中 **validated_data 是对该字典拆包
def create(self, validated_data):
profile_data = validated_data.pop('profile')
user = User.objects.create(**validated_data)
Profile.objects.create(user=user, **profile_data)
return user

def update(self, instance, validated_data):
profile_data = validated_data.pop('profile')
# 除非应用程序正确地强制始终设置该字段,否则就应该抛出一个需要处理的`DoesNotExist`。
profile = instance.profile

instance.username = validated_data.get('username', instance.username)
instance.email = validated_data.get('email', instance.email)
instance.save()

profile.is_premium_member = profile_data.get(
'is_premium_member',
profile.is_premium_member
)
profile.has_support_contract = profile_data.get(
'has_support_contract',
profile.has_support_contract
)
profile.save()

return instance

15.5 关联对象嵌套序列化

(1)PrimaryKeyRelatedField(主键)

此字段将被序列化为关联对象的主键。

1
2
3
hbook = serializers.PrimaryKeyRelatedField(label='图书', read_only=True)
# 或
hbook = serializers.PrimaryKeyRelatedField(label='图书', queryset=BookInfo.objects.all())

指明字段时需要包含read_only=True或者queryset参数:

包含read_only=True参数时,该字段将不能用作反序列化使用

包含queryset参数时,将被用作反序列化时参数校验使用

使用效果:

1
2
3
4
5
6
from booktest.serializers import HeroInfoSerializer
from booktest.models import HeroInfo
hero = HeroInfo.objects.get(id=6)
serializer = HeroInfoSerializer(hero)
serializer.data
# {'id': 6, 'hname': '乔峰', 'hgender': 1, 'hcomment': '降龙十八掌', 'hbook': 2}

(2)StringRelatedField(__str__

此字段将被序列化为关联对象的字符串表示方式(即__str__方法的返回值)

1
hbook = serializers.StringRelatedField(label='图书')

使用效果

1
{'id': 6, 'hname': '乔峰', 'hgender': 1, 'hcomment': '降龙十八掌', 'hbook': '天龙八部'}

(3)HyperlinkedRelatedField(url)

此字段将被序列化为获取关联对象数据的接口链接

1
hbook = serializers.HyperlinkedRelatedField(label='图书', read_only=True, view_name='books-detail')

必须指明view_name参数,以便DRF根据视图名称寻找路由,进而拼接成完整URL。

使用效果

1
{'id': 6, 'hname': '乔峰', 'hgender': 1, 'hcomment': '降龙十八掌', 'hbook': 'http://127.0.0.1:8000/books/2/'}

(4)SlugRelatedField(字段)

此字段将被序列化为关联对象的指定字段数据,slug_field指明使用关联对象的哪个字段

1
hbook = serializers.SlugRelatedField(label='图书', read_only=True, slug_field='bpub_date')

使用效果

1
# {'id': 6, 'hname': '乔峰', 'hgender': 1, 'hcomment': '降龙十八掌', 'hbook': datetime.date(1986, 7, 24)}

(5)使用关联对象的序列化器

1
hbook = BookInfoSerializer()

使用效果

1
# {'id': 6, 'hname': '乔峰', 'hgender': 1, 'hcomment': '降龙十八掌', 'hbook': OrderedDict([('id', 2), ('btitle', '天龙八部')te', '1986-07-24'), ('bread', 36), ('bcomment', 40), ('image', None)])}

(6)重写to_representation方法

序列化器的每个字段实际都是由该字段类型的to_representation方法决定格式的,可以通过重写该方法来决定格式。

注意,to_representations方法不仅局限在控制关联对象格式上,适用于各个序列化器字段类型。

自定义一个新的关联字段:

1
2
3
4
5
6
7
class BookRelateField(serializers.RelatedField):
"""自定义用于处理图书的字段"""
def to_representation(self, value):
return 'Book: %d %s' % (value.id, value.btitle)
# 指明hbook为BookRelateField类型

hbook = BookRelateField(read_only=True)

使用效果

1
# {'id': 6, 'hname': '乔峰', 'hgender': 1, 'hcomment': '降龙十八掌', 'hbook': 'Book: 2 天龙八部'}

16. CBV

DRF推荐使用基于类的视图(CBV)开发API, 提供了4种开发CBV开发模式

  • 使用基础APIView类
  • 使用Mixins类和GenericAPI类混配(极少使用,不推荐)
  • 使用通用视图generics.*类, 比如 generics.ListCreateAPIView
  • 使用视图集ViewSet和ModelViewSet

其中:

  • 基础APIView类:可读性最高,代码最多,灵活性最高。当你需要对的API行为进行个性化定制时,建议使用这种方式。
  • 通用generics.*类:可读性好,代码适中,灵活性较高。当你需要对一个模型进行标准的增删查改全部或部分操作时建议使用这种方式。
  • 使用视图集viewset: 可读性较低,代码最少,灵活性最低。当你需要对一个模型进行标准的增删查改的全部操作且不需定制API行为时建议使用这种方式。

(1)基础APIView类

DRF的APIView类继承了Django自带的View类, 一样可以按请求方法调用不同的处理函数,比如get方法处理GET请求,post方法处理POST请求。它不仅支持更多请求方法,而且对Django的request对象进行了封装,可以使用request.data获取用户通过POST, PUT和PATCH方法发过来的数据,而且支持插拔式地配置认证、权限和限流类,举例:

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
55
from rest_framework.views import APIView
from django.http import Http404
from .models import Article
from .serializers import ArticleSerializer


class ArticleList(APIView):
"""
List all articles, or create a new article.
"""

def get(self, request, format=None):
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)

def post(self, request, format=None):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
# 注意:手动将request.user与author绑定
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class ArticleDetail(APIView):
"""
Retrieve, update or delete an article instance.
"""

def get_object(self, pk):
try:
return Article.objects.get(pk=pk)
except Article.DoesNotExist:
raise Http404

def get(self, request, pk, format=None):
article = self.get_object(pk)
serializer = ArticleSerializer(article)
return Response(serializer.data)

def put(self, request, pk, format=None):
article = self.get_object(pk)
serializer = ArticleSerializer(instance=article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, pk, format=None):
article = self.get_object(pk)
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

最大不同的是我们不需要在对用户的请求方法进行判断。该视图可以自动将不同请求转发到相应处理方法,逻辑上也更清晰,需要修改我们的url配置, 让其指向新的基于类的视图。

1
2
3
4
5
6
7
8
9
10
11
from django.urls import re_path
from rest_framework.urlpatterns import format_suffix_patterns

from . import views

urlpatterns = [
re_path(r'^articles/$', views.ArticleList.as_view()),
re_path(r'^articles/(?P<pk>[0-9]+)$', views.ArticleDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

(2)Mixin类和GenericAPI类混配(❌)

使用基础的APIView类并没有大量简化我们的代码。如果你仔细地观察,你还会发现与增删改查操作相关的代码包括返回内容对所有模型几乎都是一样的。比如你现在需要对文章类别Category模型也进行序列化和反序列化,你只需要复制Article视图代码,将Article模型改成Category模型, 序列化类由ArticleSeralizer类改成CategorySerializer类就行了。

对于这些通用的增删改查行为,DRF已经提供了相应的Mixin类。Mixin类可与generics.GenericAPI类联用,灵活组合成你所需要的视图。

现在来使用Mixin类和generics.GenericAPI类重写我们的类视图。

# blog/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用GENERIC APIView & Mixins
from rest_framework import mixins
from rest_framework import generics

class ArticleList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)

GenericAPIView 类继承了APIView类,提供了基础的API视图。它对用户请求进行了转发,并对Django自带的request对象进行了封装。不过它比APIView类更强大,因为它还可以通过queryset和serializer_class属性指定需要序列化与反序列化的模型或queryset及所用到的序列化器类。

这里的 ListModelMixin 和 CreateModelMixin类则分别引入了.list() 和 .create() 方法,当用户发送get请求时调用Mixin提供的list()方法,将指定queryset序列化后输出,发送post请求时调用Mixin提供的create()方法,创建新的实例对象。

DRF还提供RetrieveModelMixin, UpdateModelMixin和DestroyModelMixin类,实现了对单个对象实例的查、改和删操作,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ArticleDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)

或许你现在要问已经有get, post, delete等方法了,为什么mixin类引入的方法要以list, create, retrieve, destroy方法命名呢? 这是因为请求方法不如操作名字清晰,比如get方法同时对应了获取对象列表和单个对象两种操作,使用list和retrieve方法后则很容易区分。另外post方法接受用户发过来的请求数据后,有时只需转发不需要创建模式对象实例,所以post方法不能简单等于create方法。

新的ArticleList视图类看似正确,但其实还有一个问题。 我们定义的序列化器ArticleSeralizer类并不包含author这个字段的,这是因为我们希望在创建article实例时我们将author与request.user进行手动绑定。在前面的例子中我们使用serializer.save(author=request.user)这一方法进行手动绑定。

现在使用mixin类后,我们该如何操作呢? 答案是重写perform_create方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ArticleList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)

# 将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

.perform_create这个钩子函数是CreateModelMixin类自带的,用于执行创建对象时需要执行的其它方法,比如发送邮件等功能,有点类似于Django的信号。类似的钩子函数还有UpdateModelMixin提供的.perform_update方法和DestroyModelMixin提供的.perform_destroy方法。

(3)通用视图generics.*类

将Mixin类和GenericAPI类混配,已经帮助我们减少了一些代码,但我们还可以做得更好,比如将get请求与mixin提供的list方法进行绑定感觉有些多余。DRF还提供了一套常用的将 Mixin 类与 GenericAPI类已经组合好了的视图,进一步简化我们的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# generic class-based views
from rest_framework import generics

class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

# 将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class =ArticleSerializer

generics.ListCreateAPIView类支持List、Create两种视图功能,分别对应GET和POST请求。generics.RetrieveUpdateDestroyAPIView支持Retrieve、Update、Destroy操作,其对应方法分别是GET、PUT和DELETE,其它常用generics.*类视图还包括ListAPIView, RetrieveAPIView, RetrieveUpdateAPIView等等。

(4)视图集(viewset)

使用通用视图generics.*类后视图代码已经大大简化,但是ArticleList和ArticleDetail两个类中queryset和serializer_class属性依然存在代码重复。使用视图集可以将两个类视图进一步合并,一次性提供List、Create、Retrieve、Update、Destroy这5种常见操作,这样queryset和seralizer_class属性也只需定义一次, 如下:

# blog/views.py

1
2
3
4
5
6
7
8
9
from rest_framework import viewsets

class ArticleViewSet(viewsets.ModelViewSet):
# 用一个视图集替代ArticleList和ArticleDetail两个视图
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 自行添加,将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

使用视图集后,我们需要使用DRF提供的路由router来分发urls,因为一个视图集现在对应多个urls的组合,而不像之前的一个url对应一个视图函数或一个视图类。

# blog/urls.py

1
2
3
4
5
6
7
8
9
10
11
from django.urls import re_path
from rest_framework.urlpatterns import format_suffix_patterns
from . import views
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'articles', viewset=views.ArticleViewSet)

urlpatterns = []

urlpatterns += router.urls

一个视图集对应List、Create、Retrieve、Update、Destroy这5种操作。有时候我只需要其中的几种操作,该如何实现呢?答案是在urls.py中指定方法映射即可,如下所示:

# blog/urls.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from django.urls import re_path
from rest_framework.urlpatterns import format_suffix_patterns
from . import views

article_list = views.ArticleViewSet.as_view(
{
'get': 'list',
'post': 'create'
})

article_detail = views.ArticleViewSet.as_view({
'get': 'retrieve', # 只处理get请求,获取单个记录
})

urlpatterns = [
re_path(r'^articles/$', article_list),
re_path(r'^articles/(?P<pk>[0-9]+)$', article_detail),
]

urlpatterns = format_suffix_patterns(urlpatterns)

另外DRF还提供了ReadOnlyModelViewSet这个类,仅支持list和retrive这个操作。

17.过滤 & 排序

(1)重写GenericsAPIView或viewset的get_queryset方法

此方法不依赖于任何第三方包, 只适合于需要过滤的字段比较少的模型。比如这里我们需要对文章title进行过滤,我们只需要修改ArticleList视图函数类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# blog/views.py

from rest_framework import generics
from rest_framework import permissions
from .permissions import IsOwnerOrReadOnly
from .pagination import MyPageNumberPagination

class ArticleList(generics.ListCreateAPIView):
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
pagination_class = MyPageNumberPagination

def get_queryset(self):
keyword = self.request.query_params.get('q')
if not keyword:
queryset = Article.objects.all()
else:
queryset = Article.objects.filter(title__icontains=keyword)
return queryset

# associate user with article author.
def perform_create(self, serializer):
serializer.save(author=self.request.user)

修改好视图类后,发送GET请求到/v1/articles?page=2&q=django, 你将得到所有标题含有django关键词的文章列表,这里显示一共有3条结果。

当一个模型需要过滤的字段很多且不确定时(比如文章状态、正文等等), 重写get_queryset方法将变得非常麻烦,更好的方式是借助django-filter这个第三方库实现过滤。

(2)使用django-filter

django-filter库包含一个DjangoFilterBackend类,该类支持REST框架的高度可定制的字段过滤。这也是小编推荐的过滤方法, 因为它自定义需要过滤的字段非常方便, 还可以对每个字段指定过滤方法(比如模糊查询和精确查询)。具体使用方式如下:

1.安装django-filter

1
pip install django-filter

2.将django_filters添加到INSTALLED_APPS

1
2
3
4
INSTALLED_APPS = [
...,
django_filters,
]

3.自定义FilterSet类。这里我们自定义了按标题关键词和文章状态进行过滤。

# blog/filters.py(新建)

1
2
3
4
5
6
7
8
9
import django_filters
from .models import Article

class ArticleFilter(django_filters.FilterSet):
q = django_filters.CharFilter(field_name='title', lookup_expr='icontains')

class Meta:
model = Article
fields = ('title', 'status')

如果你对django-filter中自定义FilterSet类比较陌生的话,可以先阅读下面两篇文章:

4.将自定义FilterSet类加入到View类或ViewSet,另外还需要将DjangoFilterBackend设为过滤后台,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# New for django-filter
from django_filters import rest_framework
from .filters import ArticleFilter

class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
pagination_class = MyPageNumberPagination

# new: filter backends and classes
filter_backends = (rest_framework.DjangoFilterBackend,)
filter_class = ArticleFilter

# associate request.user with author.
def perform_create(self, serializer):
serializer.save(author=self.request.user)

发送GET请求到/v1/articles?page=2&q=django&status=p, 你将得到如下返回结果,只包含发表了的文章。

你还可以看到REST框架提供了一个新的Filters下拉菜单按钮,可以帮助您对结果进行过滤(见上图标红部分)。

(3)使用DRF提供的SearchFilter类

其实DRF自带了具有过滤功能的SearchFilter类,其使用场景与Django-filter的单字段过滤略有不同,更侧重于使用一个关键词对模型的某个字段或多个字段同时进行搜索。

使用这个类,你还需要指定search_fields, 具体使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from rest_framework import filters

class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
pagination_class = MyPageNumberPagination

# new: add SearchFilter and search_fields
filter_backends = (filters.SearchFilter, )
search_fields = ('title',)

# associate request.user with author.
def perform_create(self, serializer):
serializer.save(author=self.request.user)

发送GET请求到/v1/articles?page=1&search=django, 你将得到如下结果。注意:这里进行搜索查询的默认参数名为?search=xxx。

SearchFilter类非常有用,因为它不仅支持对模型的多个字段进行查询,还支持ForeinKey和ManyToMany字段的关联查询。按如下修改search_fields, 就可以同时搜索标题或用户名含有某个关键词的文章资源列表。修改好后,作者用户名里如果有django,该篇文章也会包含在搜索结果了。

1
search_fields = ('title', 'author__username')

默认情况下,SearchFilter类搜索将使用不区分大小写的部分匹配(icontains)。你可以在search_fields中添加各种字符来指定匹配方法。

  • ‘^’开始 - 搜索。
  • ‘=’完全匹配。
  • ‘@’全文搜索。
  • ‘$’正则表达式搜索。

例如:search_fields = (‘=title’, )精确匹配title。

前面我们详细介绍了对结果进行过滤的3种方法,接下来我们再看看如何对结果进行排序,这里主要通过DRF自带的OrderingFilter类实现。

使用DRF的OrderingFilter类

使用OrderingFilter类首先要把它加入到filter_backends, 然后指定排序字段即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
from rest_framework import filters

class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
pagination_class = MyPageNumberPagination


filter_backends = (filters.SearchFilter, filters.OrderingFilter,)
search_fields = ('title',)
ordering_fields = ('create_date')

发送请求时只需要在参数上加上?ordering=create_date或者?ordering=-create_date即可实现对结果按文章创建时间正序和逆序进行排序。

点击DRF界面上的Filters按钮,你还会看到搜索和排序的选项。

18.限流

(1)限流(Throttle)概述

限流(Throttle)就是限制客户端对API 的调用频率,是API开发者必须要考虑的因素。比如个别客户端(比如爬虫程序)短时间发起大量请求,超过了服务器能够处理的能力,将会影响其它用户的正常使用。又或者某个接口占用数据库资源比较多,如果同一时间该接口被大量调用,服务器可能会陷入僵死状态。为了保证API服务的稳定性,并防止接口受到恶意用户的攻击,我们必须要对我们的API服务进行限流。

DRF中限制对API的调用频率非常简便,它为我们主要提供了3个可插拔使用的限流类,分别是AnonRateThrottle, UserRateThrottle和ScopeRateThrottle类。

  • AnonRateThrottle 用于限制未认证用户的请求频率,主要根据用户的 IP地址来确定用户身份。
  • UserRateThrottle 用于限定认证用户的请求频率,可对不同类型的用户实施不同的限流政策。
  • ScopeRateThrottle可用于限制对 API 特定部分的访问。只有当正在访问的视图包含 throttle_scope 属性时才会应用此限制。这个与UserRateThrottle类的区别在于一个针对用户限流,一个针对API接口限流。

DRF限制频率的指定格式为 “最大访问次数/时间间隔”,例如设置为 5/min,则只允许一分钟内最多调用接口 5 次。其它常用格式包括”10/s”, “100/d”等。超过限定次数的调用将抛出 exceptions.Throttled 异常,客户端收到 429 状态码(too many requests)的响应。

全局使用限流类

在前面的案例中,当你访问/v1/articles/,你将得到如下所示文章资源列表。这个接口是没有限流的,所以在短时间内你无论刷新多少次浏览器,也不会得到报错。现在我们要对这个接口增加限流,匿名用户请求频率限制在”2/min”,即一分钟两次,而认证用户请求频率限制在”10/min”, 即一分钟10次。

最简单的使用DRF自带限流类的方法,就是在settings.py中进行全局配置,如下所示。一是要设置需要使用的限流类,二是要设置限流范围(scope)及其限流频率。AnonRateThrottle和UserRateThrottle默认的scope分别为”anon”和”user”。该配置会对所有API接口生效。

1
2
3
4
5
6
7
8
9
10
11
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',

],
'DEFAULT_THROTTLE_RATES': {
'anon': '2/min',
'user': '10/min'
}
}

现在短时间内快速刷新你的浏览器或通过postman发送多个请求到/v1/articles,如果你是匿名用户,当你的每分钟请求数量累计达到2次时,你将看到如下返回信息。如果你是认证用户,你的每分钟请求数量达到10次时才会被限流。当你访问其它接口,会受到同样限流限制。

(2)视图类或视图集中使用限流类

DRF中还可以在单个视图或单个视图集中进行限流配置,单个视图中的配置会覆盖全局设置。现在我们希望保留settings.py的限流全局配置,并专门为文章资源列表/v1/articles定制一个限流类,新的访问频率限制为匿名用户为”5/min”, 认证用户为”30/min”,该配置仅对文章资源列表这个接口生效。

我们首先在app文件夹blog目录下新建throttles.py, 添加如下代码:

1
2
3
4
5
6
7
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle

class ArticleListAnonRateThrottle(AnonRateThrottle):
THROTTLE_RATES = {"anon": "5/min"}

class ArticleListUserRateThrottle(UserRateThrottle):
THROTTLE_RATES = {"user": "30/min"}

我们通过继承自定义了ArticleListAnonRateThrottle, ArticleListUserRateThrottle两个类,并通过THROTTLE_RATES属性设置了新的访问频率限制。现在我们可以将它们应用到views.py中对应文章资源列表的API视图类。无需重启测试服务器,你将发现新的限流设置已经生效了。

1
2
3
4
5
6
7
8
from .throttles import ArticleListAnonRateThrottle, ArticleListUserRateThrottle

class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
pagination_class = MyPageNumberPagination
throttle_classes = [ArticleListAnonRateThrottle, ArticleListUserRateThrottle]

有时对一个认证用户进行限流不仅要限制每分钟的请求次数,还需要限制每小时的请求次数,这时该如何操作呢? 我们可以自定义两个UserRateThrottle子类,并设置不同的scope,如下所示:

1
2
3
4
5
6
7
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle

class MinuteUserRateThrottle(UserRateThrottle):
scope = 'limit_per_minute'

class HourUserRateThrottle(UserRateThrottle):
scope = 'limit_per_hour'

然后修改我们的视图类,使用新的限流类:

1
throttle_classes = [HourUserRateThrottle, MinuteUserRateThrottle]

现在我们只指定了限流类,还未设置限流频率,那么到哪里去设置呢? 你可以去settings.py中设置,也可以在自定义限流类中通过THROTTLE_RATES属性指定。

1
2
3
4
5
6
7
8
9
10
11
12
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '2/min',
'user': '10/min',
'limit_per_minute':'5/min', # 新增
'limit_per_hour': '500/hour', # 新增
}
}

(3)如何使用ScopeRateThrottle类

AnonRateThrottle和UserRateThrottle类都是针对单个用户请求进行限流的,而ScopeRateThrottle类是针对不同API接口资源进行限流的,限制的是所有用户对接口的访问总数之和。使用时直接在视图类里通过throttle_scope 属性指定限流范围(scope), 然后在settings.py对不同scope设置限流频率。例子如下所示:

1
2
3
4
5
class ArticleListView(APIView):
throttle_scope = 'article_list'

class ArticleDetailView(APIView):
throttle_scope = 'article_detail'

针对不同api接口设置不同限流频率。如下配置代表文章资源列表一天限1000次请求(所有用户访问数量之和),文章详情接口限1小时100次。

1
2
3
4
5
6
7
8
9
10
11
12
13
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
'rest_framework.throttling.ScopedRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '2/min',
'user': '10/min',
'article_list':'1000/day', # 新增
'article_detail': '100/hour', # 新增
}
}

(4)如何自定义复杂的限流类

有时你还需要自定义限流类。这时你需要继承BaseThrottle类、SimpleRateThrottle或者UserRateThrottle类,然后重写*allow_request(self, request, view)或者get_rate(self, request=none)*方法。DRF给的示例方法如下所示,该限流类10个请求中只允许一个通过。

1
2
3
4
5
import random

class RandomRateThrottle(throttling.BaseThrottle):
def allow_request(self, request, view):
return random.randint(1, 10) != 1

常见 HTTP 请求行为

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • DELETE(DELETE):从服务器删除资源。
  • PATCH(UPDATE):在服务器更新(更新)资源(客户端提供改变的属性)。
  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

HTTP 状态码

服务器向用户返回的状态码和提示信息:

  • 200 OK - [GET]:服务器成功返回用户请求的数据
  • 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据操作
  • 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 - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

基础实践

Install

1
2
pip install django
pip install djangorestframework

创建项目

1
django-admin startproject "project_name"

创建应用

1
python manage.py startapp "app_name"

创建数据库文件

1
python manage.py makemigrations

数据库迁移

1
python manage.py migrate

启动项目

1
python manage.py runserver

新建模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.db import models

# Create your models here.


class Comment(models.Model):
"""
评论
"""
email = models.EmailField()
content = models.TextField()
created = models.CharField(max_length=100)
objects = models.Manager()

def __str__(self):
return f'{self.content}'

更新配置

1
2
3
4
5
INSTALLED_APPS = [    
...
'rest_framework',
'comment',
]

更新URL

1
2
3
4
5
from django.urls import path, include
urlpatterns = [
path('comment/', include('comment.urls')),
...
]

新增应用API: apis.py

1
2
3
4
5
6
7
8
9
10
11
from rest_framework import generics

from .serializers import CommentSerializer
from .models import Comment


class List(generics.ListCreateAPIView):
serializer_class = CommentSerializer

def get_queryset(self):
return Comment.objects.all()

新增应用URL: urls.py

1
2
3
4
5
6
from django.urls import path
from .apis import List

urlpatterns = [
path('', List.as_view(), name='list')
]

创建管理员身份

1
python manage.py createsuperuser

Centos配置Django

更新系统软件包

1
yum update -y

安装软件管理包

1
2
3
yum -y groupinstall "Development tools"

yum install openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel psmisc libffi-devel

下载python3 到 /usr/local目录

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
cd /usr/local
wget https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tgz

//解压
tar -zxvf Python-3.6.6.tgz

//进入
cd Python-3.6.6

// 编译
./config --prefix=/usr/local/python3

// 安装
make
make install

//建立软链接,在终端也可以直接使用python3
ln -s /usr/local/python3/bin/python3.6 /usr/bin/python3

//为pip建立软链接
ln -s /usr/local/python3/bin/pip3.6 /usr/bin/pip3

//查看是否安装成功
python3 -V
pip3 -V

安装virtualenv

1
2
3
pip3 install virtualenv

ln -s /usr/local/python3/bin/virtualenv /usr/bin/virtualenv

新建虚拟环境目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mkdir -p data/env

cd data/env

virtualenv --python=/usr/bin/python3 djangoenv // djangoenv是自定义名称

// 启动虚拟环境
source activate

//虚拟环境中安装django uwsgi
pip3 install django
pip3 install uwsgi

ln -s /usr/local/python3/bin/uwsgi /usr/bin/uwsgi

将项目目录下环境依赖导出到requirements.txt

1
pip freeze > requirements.txt

项目上传 基于fileziila并解压

1
2
3
yum install -y unzip zip

unzip file.zip

导入数据库

1
mysql -u root -p

创建与项目中相应名称的数据库

1
2
3
4
5
create database guojunmingblog

use guojuningblog

source /SQL文件目录地址

run

1
python3 manage.py runserver

配置uwsgi文件,例如项目名 guojunmingblog,则在项目根目录下创建 guojunmingblog.xml

1
2
3
4
5
6
7
<uwsgi>
<socket>127.0.0.1:8000</socket> <!-- 内部端口,自定义 -->
<chdir>/root/blog/kuls_blog</chdir> <!-- 项目路径 -->
<module>kuls_blog.wsgi</module> <!-- mysite为wsgi.py所在目录名-->
<processes>4</processes> <!-- 进程数 -->
<daemonize>uwsgi.log</daemonize> <!-- 日志文件 -->
</uwsgi>

安装nginx和配置nginx.conf

1
2
3
4
5
6
7
8
9
10
cd /home/
wget http://nginx.org/download/nginx-1.13.7.tar.gz
// 执行解压命令
tar -zxvf nginx-1.13.7.tar.gz
// 解压后进入文件夹
./configure
make
make install
// 备份conf文件
cp nginx.conf nginx.conf.bak

编辑conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 80;
server_name 127.0.0.1:80; #改为自己的域名,没域名修改为127.0.0.1:80 charset utf-8;
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8000; #端口要和uwsgi里配置的一样
uwsgi_param UWSGI_SCRIPT kuls_blog.wsgi; #wsgi.py所在的目录名+.wsgi uwsgi_param UWSGI_CHDIR /root/blog/kuls_blog; #项目路径
}
location /static/ {
alias /root/blog/kuls_blog/static/; #静态资源路径 }
} }

进入/usr/local/nginx/sbin目录执行

1
./nginx

查看Uwsgi进程

1
ps -ef|grep uwsgi

用kill方法把uwsgi进程杀死,再启动uwsgi

1
killall -9 uwsgi

启动方法

1
uwsgi -x mysite.xml

nginx平滑重启

1
/usr/local/nginx/sbin/nginx -s reload

项目中应用含有静态文件,需要在setting.py进行STATIC_ROOT

1
STATIC_ROOT = os.path.join(BASE_DIR, 'static') # 指定样式收集目录

执行下面指令,自动把静态文件收集到/static/目录下

1
python manage.py collectstatic

mysql开机启动

1
2
3
4
5
6
//设置开机启动
systemctl enable mysqld.service
//检查是否设置成功
systemctl list-unit-files | grep mysqld
//开启MYSQL服务
systemctl start mysqld.service

修改密码

1
2
3
4
5
6
7
8
//查看密码
grep 'temporary password' /var/log/mysqld.log
//登录
mysql -u root -p
//修改
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'MyNewPassword';
//命令生效
mysql> flush privileges;

mysql端口被占用

1
2
3
//查看3306端口是否被占用
netstat -nlt|grep mysql
// kill -9 pid

外网访问问题

1
2
3
4
5
6
7
8
9
#远程设置
mysql> use mysql;
mysql> update user set host='%' where user='root';
#授权用户名的权限,赋予任何主机访问数据的权限
mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; mysql> FLUSH PRIVILEGES;
# 设置密码
mysql> ALTER USER 'root'@'%' IDENTIFIED BY 'Root!2018' PASSWORD EXPIRE NEVER;
mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'Root!2018';
mysql> FLUSH PRIVILEGES;

关闭防火墙

1
2
3
4
# 关闭防火墙
sudo systemctl stop firewalld.service
# 关闭开机启动
sudo systemctl disable firewalld.service

Markdown 编辑器

Github地址**:** https://github.com/pylixm/django-mdeditor

Django-mdeditor是基于editor.md的一个django Markdown 文本编辑插件应用。

Django-mdeditor的灵感参考自项目 django-ckeditor https://github.com/django-ckeditor/django-ckeditor

1
2
pip install django-mdeditor # 用于后台编辑
pip install markdown # 用于前端显示

settings配置

1
2
3
4
5
INSTALLED_APPS = ['meditor']

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MEDIA_URL = '/media/' # 上传的文件和图片都会默认保存在/media/editor下

url配置

1
2
3
4
5
6
7
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [path('mdeditor/', include('mdeditor'))]

if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

model配置

1
2
3
4
class Article(model.Model):
...
body = MDTextField()

admin配置

1
admin.site.register(Article)

接口配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# markdown.extensions.extra: 用于标题、表格、引用这些 基本转换
# markdown.extensions.codehilite: 用于语法高亮
# markdown.extensions.toc: 用于生成目录

# eg.
from .models import Post
def post_article(request, pk):
post = get_object_or_404(Post, pk)
post.body markdown.markdown(post.body,
extensions=[
'markdown.extensions.extra'
'markdown.extensions.codehilite'
'markdown.extensions.toc',
])
return render(request, 'blog/detail.html',context={'post': post})


QA

  1. POST URL重定向
1
2
3
4
Q: RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining POST data. Change your form to point to 127.0.0.1:8000/project/636/orchestrations/5/versions/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.
2021-03-10 19:04:52,486 - ERROR - "POST /project/636/orchestrations/5/versions HTTP/1.1" 500 68836
A: 1.确保form表单的action或URL是否以/结尾,例如127.0.0.1:8000/version/
2.修改settings.py,添加以下内容 APPEND_SLASH=False
  1. TemplateDoesNotExist
1
2
Q: Django: TemplateDoesNotExist (rest_framework/api.html)
A: Make sure you have rest_framework in your settings's INSTALLED_APPS