用rest-framework快速实现的API+DOC以及token验证

🤠摘要

本文以一个简单的例子来举例说明,如何在Django中使用rest-framework来创建API,以及生成对应的doc文档,并开启基于Token的验证。
本文中使用rest-framework的viewset来创建视图,其他视图的创建方法后面的文章会有说明哦~

让我们直接开始~~~~

🤠项目基础环境

本文实验环境说明:
python 3.6.2django==1.11.5djangorestframework==3.73coreapi==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_APPSREST_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方法

  • 在我们的informationsapp下创建一个新的文件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验证功能/

posted @ 2020-03-19 08:36  我的胡子有点扎  阅读(621)  评论(0编辑  收藏  举报