用rest-framework快速实现的API+DOC以及token验证
🤠摘要
本文以一个简单的例子来举例说明,如何在Django中使用rest-framework来创建API,以及生成对应的doc文档,并开启基于Token的验证。
本文中使用rest-framework的viewset来创建视图,其他视图的创建方法后面的文章会有说明哦~
让我们直接开始~~~~
🤠项目基础环境
本文实验环境说明:
python 3.6.2
,django==1.11.5
,djangorestframework==3.73
,coreapi==2.3.3
- 项目目录
mkdir UserInfo
cd UserInfo
- 创建virtualenv
virtualenv rest-env
source env/bin/activate
- 模块安装
pip install django==1.11
pip install djangorestframework==3.73
- 开启一个新的Django项目,新的app
django-admin.py startproject UserInfo .
cd UserInfo
python manage.py startapp informations
- makemigration&migrate数据库创建
python manage.py makemigration
python manage.py migrate
- 创建用户
python manage.py createsuperuser
# 此处我们创建的用户admin:admin123
分页
- 分页设置
from rest_framework.pagination import PageNumberPagination
class CommonPageNumberPagination(PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 100
- settings设置
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'lib.pagination.CommonPageNumberPagination',
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
}
🤠数据库表相关
- models.py
from django.db import models
# Create your models here.
class UserInfo(models.Model):
username = models.CharField(max_length=64, unique=True, verbose_name="用户名")
age = models.IntegerField(verbose_name="年龄")
email = models.EmailField(verbose_name='邮箱')
user_type = models.ForeignKey('UserType')
def __str__(self):
return self.username
class Mate:
verbose_name = "用户信息"
verbose_name_plural = verbose_name
class UserType(models.Model):
type_name = models.CharField(max_length=64, unique=True, verbose_name="类型名称")
operator = models.ForeignKey('auth.User')
# 此处的外键用户为操作者,为django的auth.User表中数据,以便于后续对权限的管理
def __str__(self):
return self.type_name
class Mate:
verbose_name = "用户类型"
verbose_name_plural = verbose_name
- app中创建一个新的文件来做序列化
serializers.py
from informations import models
from rest_framework import serializers
class UserInfoSerializers(serializers.HyperlinkedModelSerializer):
'''本例中使用了基于超链接的序列化方式'''
class Meta:
model = models.UserInfo
fields = ('id', 'username', 'age', 'email', 'user_type')
class UserTypeSerializers(serializers.HyperlinkedModelSerializer):
'''本例中使用了基于超链接的序列化方式'''
operator = serializers.ReadOnlyField(source='operator.username')
class Meta:
model = models.UserType
fields = ('id', 'type_name', 'operator')
- views.py
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view, APIView
from rest_framework import viewsets
from informations import models
from informations import serializers
from rest_framework import permissions
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
from rest_framework.authtoken.views import ObtainAuthToken
import datetime
from django.utils.timezone import utc
class ObtainExpiringAuthToken(ObtainAuthToken):
'''重写了基于token认证的方法'''
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.validated_data['user'])
utc_now = datetime.datetime.utcnow().replace(tzinfo=utc)
EXPIRE_MINUTES = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES', 1)
if created or token.created < utc_now - datetime.timedelta(minutes=EXPIRE_MINUTES):
token.delete()
token = Token.objects.create(user=serializer.validated_data['user'])
token.created = utc_now
token.save()
return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserInfoViewSet(viewsets.ModelViewSet):
'''用户信息详情'''
queryset = models.UserInfo.objects.all()
serializer_class = serializers.UserInfoSerializers
permission_classes = (permissions.IsAuthenticated,)
# 权限这快,使用了必须认证才可以查看的方式
class UserTypeViewSet(viewsets.ModelViewSet):
'''用户类型详情'''
queryset = models.UserType.objects.all()
serializer_class = serializers.UserTypeSerializers
permission_classes = (permissions.IsAuthenticated,)
# 权限这快,使用了必须认证才可以查看的方式
def perform_create(self, serializer):
''''''
serializer.save(operator=self.request.user)
- app中的urls.py
from django.conf.urls import url, include
from rest_framework.authtoken import views as authtoken_views
from rest_framework.routers import DefaultRouter
from rest_framework.schemas import get_schema_view
from rest_framework.documentation import include_docs_urls
from informations import views
schema_view = get_schema_view(title="用户信息")
router = DefaultRouter()
router.register(r'user_info', views.UserInfoViewSet, )
router.register(r'user_type', views.UserTypeViewSet, )
obtain_expiring_auth_token = views.ObtainExpiringAuthToken.as_view()
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^schema/$', schema_view),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^docs/', include_docs_urls(title="用户信息API文档")),
url(r'^token-auth/', obtain_expiring_auth_token,name="api-token"),
]
- 项目urls.py
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^api/', include("informations.urls")),
]
- settings.py设置,主要设置
INSTALLED_APPS
与REST_FRAMEWORK
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'informations', # app
'rest_framework', # rest_framework
'rest_framework.authtoken' # token认证
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAdminUser'],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework.authentication.TokenAuthentication','rest_framework.authentication.SessionAuthentication',), # 覆盖默认设定,使用token认证和session的认证方式
'PAGE_SIZE': 3, # 分页设置,单页数量
}
REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES = 60 # token超时时间
注意
此时我们重写了obtain_expiring_auth_token方法来做基于token的验证,但是还有一个问题就是假如用户验证后生成一个token,然后用户在过期时间后再也不来请求这个接口重新生成新的token,那么这个用户的token也会一直在数据中保存生效且不会被更新,那么要需要结合token验证函数,来强制删除用户过期的token。
并且采用Django内置的缓存机制,将我们生成的有效的token放在cache中便于使用,而不是每次验证都需要去数据库的authtoken_token表中查询,减少数据库查询压力
所以我们可以这样做:重写authentication.authenticate_credentials
方法
- 在我们的
informations
app下创建一个新的文件authentication.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author : Leon
# Python version
# Date 2017/12/12
import datetime
from django.conf import settings
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
import pytz
from django.core.cache import cache
EXPIRE_MINUTES = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES', 1)
class ExpiringTokenAuthentication(TokenAuthentication):
"""Set up token expired time"""
def authenticate_credentials(self, key):
cache_user = cache.get(key)
if cache_user:
return cache_user, key
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token')
if not token.user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
# This is required for the time comparison
utc_now = datetime.datetime.utcnow()
utc_now = utc_now.replace(tzinfo=pytz.utc)
if token.created < utc_now - datetime.timedelta(minutes=EXPIRE_MINUTES):
token.delete()
raise exceptions.AuthenticationFailed('Token has expired')
if token:
cache.set(key, token.user, EXPIRE_MINUTES * 60)
return token.user, token
- settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAdminUser'],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_AUTHENTICATION_CLASSES': (
# 注释掉默认token方式
# 'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
# 使用重写的带有过期的cache的token认证
'informations.authentication.ExpiringTokenAuthentication',
),
'PAGE_SIZE': 3,
}
REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES = 60
这样我们就实现了token再规定时间自动更新删除,token存于cache中,减少了数据库查询压力,是不是很吊的样子,快去试试吧~~~ 🤓🤓🤓
当然我在gayhub🤔🤔🤔上发现了一个大神写好的项目适用于好几个版本的Django,只需要pip安装即可!大家可以自己去尝试探索一下,链接在这:戳下我
设置标准返回
from rest_framework.views import exception_handler
from rest_framework.views import Response
from rest_framework import status
from rest_framework.renderers import JSONRenderer
# 设置自定义的统一返回数据结构格式
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response:
message = None
for index, value in enumerate(response.data):
if index == 0:
key = value
value = response.data[key]
if isinstance(value, str):
message = value
else:
message = key + value[0]
if response is None:
return Response({
'message': '服务器错误',
"success": False,
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=True)
if response.status_code > 399:
return Response({
'message': message,
"success": False,
}, status=response.status_code, exception=True)
else:
return Response({
'message': message,
"success": True,
}, status=response.status_code, exception=True)
else:
return response
class CustomRenderer(JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
if renderer_context:
ret = {
'msg': "",
'code': "",
"success": True,
'data': data,
}
if isinstance(data, dict):
if renderer_context["response"].status_code > 399:
ret["msg"] = data.pop('message', 'success')
ret["success"] = False
ret["code"] = renderer_context["response"].status_code
ret["data"] = ""
return super().render(ret, accepted_media_type, renderer_context)
else:
ret["msg"] = data.pop('message', 'success')
ret["success"] = True
ret["code"] = renderer_context["response"].status_code
return super().render(ret, accepted_media_type, renderer_context)
else:
ret["msg"] = 'success'
ret["code"] = 0
return super().render(ret, accepted_media_type, renderer_context)
else:
return super().render(data, accepted_media_type, renderer_context)
- settings
REST_FRAMEWORK = {
# 自定义返回结果
"EXCEPTION_HANDLER": "lib.response.custom_exception_handler",
"DEFAULT_RENDERER_CLASSES": (
"lib.response.CustomRenderer",
)
}
🤠测试
- 获取token
LeonMacBookPro:~ Leon$ http POST 127.0.0.1:8000/api/token-auth/ username='admin' password='admin123'
HTTP/1.0 200 OK
Allow: POST, OPTIONS
Content-Length: 52
Content-Type: application/json
Date: Mon, 11 Dec 2017 12:25:25 GMT
Server: WSGIServer/0.2 CPython/3.6.2
X-Frame-Options: SAMEORIGIN
{
"token": "67e39a03e2afbfb6ddb86e7ff98c2b40c91ac612"
}
- 使用token操作数据,此处使用
httpie
模块
LeonMacBookPro:~ Leon$ http GET http://127.0.0.1:8000/api/user_info/1/ "Authorization: Token 67e39a03e2afbfb6ddb86e7ff98c2b40c91ac612"
HTTP/1.0 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Length: 118
Content-Type: application/json
Date: Mon, 11 Dec 2017 12:26:31 GMT
Server: WSGIServer/0.2 CPython/3.6.2
Vary: Accept
X-Frame-Options: SAMEORIGIN
{
"age": 22,
"email": "4613892@cive.com",
"id": 1,
"user_type": "http://127.0.0.1:8000/api/user_type/1/",
"username": "Leon"
}
----------------------------------------------------------------------
LeonMacBookPro:~ qiumeng$ http GET http://127.0.0.1:8000/api/user_info/ "Authorization: Token 67e39a03e2afbfb6ddb86e7ff98c2b40c91ac612"
HTTP/1.0 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 455
Content-Type: application/json
Date: Mon, 11 Dec 2017 12:34:00 GMT
Server: WSGIServer/0.2 CPython/3.6.2
Vary: Accept
X-Frame-Options: SAMEORIGIN
{
"count": 4,
"next": "http://127.0.0.1:8000/api/user_info/?limit=3&offset=3",
"previous": null,
"results": [
{
"age": 22,
"email": "4613892@cive.com",
"id": 1,
"user_type": "http://127.0.0.1:8000/api/user_type/1/",
"username": "Leon"
},
{
"age": 22,
"email": "qiumeng@e.cn",
"id": 2,
"user_type": "http://127.0.0.1:8000/api/user_type/2/",
"username": "leon123"
},
{
"age": 44,
"email": "forsaken627@1111.com",
"id": 3,
"user_type": "http://127.0.0.1:8000/api/user_type/4/",
"username": "alex"
}
]
}
🤠docs展示
🤠schema说明
浏览器访问:http://127.0.0.1:8000/api/schema/
{
"_meta": {
"title": "用户信息",
"url": "http://127.0.0.1:8000/api/schema/?format=corejson"
},
"_type": "document",
"token-auth": {
"create": {
"_type": "link",
"action": "post",
"url": "/api/token-auth/"
}
},
"user_info": {
"create": {
"_type": "link",
"action": "post",
"encoding": "application/json",
"fields": [
{
"location": "form",
"name": "username",
"required": true,
"schema": {
"_type": "string",
"description": "",
"title": "用户名"
}
},
{
"location": "form",
"name": "age",
"required": true,
"schema": {
"_type": "integer",
"description": "",
"title": "年龄"
}
},
{
"location": "form",
"name": "email",
"required": true,
"schema": {
"_type": "string",
"description": "",
"title": "邮箱"
}
},
{
"location": "form",
"name": "user_type",
"required": true,
"schema": {
"_type": "string",
"description": "",
"title": "User type"
}
}
],
"url": "/api/user_info/"
},
"delete": {
"_type": "link",
"action": "delete",
"fields": [
{
"location": "path",
"name": "id",
"required": true,
"schema": {
"_type": "integer",
"description": "A unique integer value identifying this user info.",
"title": "ID"
}
}
],
"url": "/api/user_info/{id}/"
},
.......
🤠页面展示
本文参考链接,再次表示感谢~~
https://stackoverflow.com/questions/14567586/token-authentication-for-restful-api-should-the-token-be-periodically-changed
http://www.xiaomastack.com/2017/03/31/优化django-rest-framework-的token验证功能/