建立API
在之前的章节,建立了一个学生注册系统和选课系统。然后使用Django的缓存框架完成了对显示课程内容的缓存。在这一章里有如下内容:
- 建立RESTful API
- 管理API视图的认证与权限
- 建立API视图集和路由
建立RESTul API
你可能会想建立一个接口(API),让其他应用程序和我们的网站进行交互。通过建立一个API,就可以让第三方应用程序自动化的操作和消费我们网站生产的数据。
有很多种方式可以建立这样一个API,推荐根据REST原则来建立这样一个API。REST是Representational State Transfer的简称,关于什么是RESTful,发现知乎上的一个回答很不错。简单的说,URL用于表示网站所有的资源,HTTP的请求种类比如GET,POST,PUT或DELETE表示动作。
通过一个HTTP请求头部的内容外加网站的URL就表示一个完整的动作。不同的HTTP响应码表示这次动作的完成结果,例如2XX表示该操作成功,4XX表示错误等。RESTful是一种风格而不是具体要求,只要符合该风格的API就是RESTful API。
RESTful API常用的数据交换格式是JSON或者XML,我们准备建立一个使用JSON进行数据交换的API。我们的API会提供以下功能:
- 获取主题
- 获取可用的课程
- 获取课程内容
- 在一个课程中注册
我们可以从0开始写视图来建立该API,也可以通过第三方应用简单的为项目建立API,在Django中最出名的第三方应用就是 Django REST framework。
安装Django REST framework
Django REST framework的官方网站是https://www.django-rest-framework.org/。
打开系统shell输入如下命令:
pip install djangorestframework==3.8.2
然后编辑settings.py激活应用:
INSTALLED_APPS = [ # ... 'rest_framework', ]
再在settings.py中加入如下设置:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
]
}
原书代码这里少了一个左方括号。
REST_FRAMEWORK用于提供API具体的设置。REST 框架提供了很多设置:DEFAULT_PERMISSION_CLASSES提供了对于增删改查行为的默认权限。我们设置了DjangoModelPermissionsOrAnonReadOnly作为唯一默认的权限类。
这个类依赖于Django的权限系统,让用户可以增删改查数据对象,同时让未登录用户只能进行只读操作。在下边的向视图增加权限中还会详细学习这部分功能。
Django REST 框架的全部设置可以在这里找到。
设置序列化器
在设置好框架后,还需要确定使用的序列化器。网站对外提供的数据应当是经过序列化的标准数据,同时还需要对外界输入的数据进行反序列化。REST 框架提供了下列类用于对一个单独对象设置序列化器:
- Serializer:为普通的Python 类实例提供序列化
- ModelSerializer:为数据模型的实例提供序列化
- HyperlinkedModelSerializer:与ModelSerializer的功能相同,但可以通过链接来表示对象之间的关系,而不是通过主键关联。
让我们来实际建立一个序列化器。在courses应用中建立如下文件结构:
api/ __init__.py serializers.py
建立了一个叫做API的包,然后打算在这个包里建立序列化器。编辑serializers.py:
from rest_framework import serializers from ..models import Subject class SubjectSerializer(serializers.ModelSerializer): class Meta: model = Subject fields = ['id', 'title', 'slug']
这是继承了ModelSerializer类的,专门用于Subject模型的序列化器。定义序列化器的类使用起来和Form以及ModelForm类很类似:Meta内的属性允许指定要序列化的类及字段。如果不设置具体的fields属性,则默认会包含该模型的全部字段。
来实验一下这个序列化器,打开Python shell:
python manage.py shell
输入以下命令:
>>> from courses.models import Subject >>> from courses.api.serializers import SubjectSerializer >>> subject = Subject.objects.latest('id') >>> serializer = SubjectSerializer(subject) >>> serializer.data {'id': 4, 'title': 'Programming', 'slug': 'programming'}
这里先得到了一个Subject实例,然后将其序列化并查看序列化后的结果。实际的输出与自己设置的Subject相关,但可以看到结果确实已经被序列化。
理解解析器(parser)与渲染器(renderer)
科班出身的朋友肯定对parser不会陌生。序列化的结果在通过HTTP 响应返回之前,必须必须渲染成为一个特殊的格式。同样,在从HTTP请求中获取数据的时候,也必须解析数据然后反序列化。REST框架包含了渲染器和解析器用于处理这些过程。
先来看看如何解析数据,在 Python shell中输入下列命令:
>>> from io import BytesIO >>> from rest_framework.parsers import JSONParser >>> data = b'{"id":4,"title":"Programming","slug":"programming"}' >>> JSONParser().parse(BytesIO(data)) {'id': 4, 'title': 'Programming', 'slug': 'programming'}
可以看到,给定一个二进制字节流形式的JSON字符串,使用JSONParser可以将其反序列化为Python的数据对象。这里如果用type查看反序列化后的结果,可以看到类型是dict。
REST框架还包含渲染器用于格式化输出API的响应。框架通过内容协商机制来确定使用哪种渲染器,会通过HTTP请求的Accept头部字段来确定这个请求所需要的内容类型来进行判断。还可以通过URL的格式化的前缀来判断,例如,一个访问可能会触发JSONParser来返回一个JSON字符串。
再回到shell中,在刚才的代码的基础上继续执行下列代码:
>>> from rest_framework.renderers import JSONRenderer >>> JSONRenderer().render(serializer.data) b'{"id":4,"title":"Programming","slug":"programming"}'
可以看到将其渲染成了二进制的字节流。使用JSONRenderer将Python数据对象渲染成JSON字符串。REST框架默认使用两个不同的渲染器:JSONRenderer 和 BrowsableAPIRenderer。后者提供了一个浏览API返回数据的web界面。可以通过在settings.py中的DEFAULT_RENDERER_CLASSES中设置REST_FRAMEWORK的相关内容来更改默认的渲染器。
关于渲染器和解析器的更多说明可以看 https://www.django-rest-framework.org/api-guide/renderers/ 和 https://www.django-rest-framework.org/api-guide/parsers/。
建立列表和详情视图
REST框架包含一系列内置的通用视图和mixin用于建立API视图,提供了增删改查数据模型对象的功能。关于所有的通用视图和mixin可以看 https://www.django-rest-framework.org/api-guide/generic-views/。
现在来建立一个获取Subject实例的视图,在courses/api/目录内建立views.py,在其中增加下列代码:
from rest_framework import generics from ..models import Subject from .serializers import SubjectSerializer class SubjectListView(generics.ListAPIView): queryset = Subject.objects.all() serializer_class = SubjectSerializer class SubjectDetailView(generics.RetrieveAPIView): queryset = Subject.objects.all() serializer_class = SubjectSerializer
在这段代码中,使用了REST框架提供的ListAPIView 和 RetrieveAPIView 两个类,在URL中包含一个主键参数,用于获取具体的数据对象。两个视图都有下列属性:
- queryset:基础的QuerySet,用于返回数据
- serializer_class:序列化器对象,指定要使用的序列化器
接下来为新的视图配置URL,在courses/api/下建立一个urls.py文件,然后编辑其中的内容:
from django.urls import path from . import views app_name = 'courses' urlpatterns = [ path('subjects/', views.SubjectListView.as_view(), name='subject_list'), path('subjects/<pk>/', views.SubjectDetailView.as_view(), name='subject_detail'), ]
然后编辑根urls.py加上一行:
urlpatterns = [ # ...... path('api/', include('courses.api.urls', namespace='api')), path('', CourseListView.as_view(), name='course_list'), ]
注意该行要加在CourseListView的上边。对于和API URLS相关的路由使用了api命名空间。然后启动站点,之后到命令行中使用:
curl http://127.0.0.1:8000/api/subjects/
会得到和下边很相似的响应:
[ {"id":1,"title":"Mathematics","slug":"mathematics"}, {"id":2,"title":"Music","slug":"music"}, {"id":3,"title":"Physics","slug":"physics"}, {"id":4,"title":"Programming","slug":"programming"} ]
这个HTTP响应包含一系列JSON格式的字符串,其内容是序列化后的所有Subject类中的数据,包含指定的三个字段。如果系统中没有安装curl,可以通过 https://curl.haxx.se/dlwiz/ 进行安装。也可以通过其他浏览器扩展比如 Postman,在https://www.getpostman.com/进行安装。
现在不使用curl,而是直接在浏览器中打开 http://127.0.0.1:8000/api/subjects/ ,会看到如下页面:
这个界面就是由之前提到的BrowsableAPIRenderer渲染器提供的。页面内显示了结果的头部信息和内容和API的返回信息。还可以通过在URL中包含具体的ID来获取一个Subject对象,打开 http://127.0.0.1:8000/api/subjects/1/ 可以发现页面只展示了一个单独的对象。
这个就叫做符合REST风格的API,即网站URL就是资源,/subjects/这个地址,从字面意思来看,访问该地址应该返回所有的subjects,我们也确实通过API返回了序列化后的所有Subjects对象。
建立嵌套的序列化器
我们再为Course类建立一个序列化器,打开courses/api/serializers.py继续编辑:
from ..models import Course class CourseSerializer(serializers.ModelSerializer): class Meta: model = Course fields = ['id', 'subject', 'title', 'slug', 'overview', 'created', 'owner', 'modules']
之后看一下Course序列化器是如何工作的,进入Python shell 输入下列命令:
>>> from rest_framework.renderers import JSONRenderer >>> from courses.models import Course >>> from courses.api.serializers import CourseSerializer >>> course = Course.objects.latest('id') >>> serializer = CourseSerializer(course) >>> JSONRenderer().render(serializer.data)
这个时候可以看到查询结果里,该课程包含的模块是一个主键列表的形式,类似这样:
"modules": [6, 7, 9, 10]
这样的数据意义不大,我们想在结果里包含每个Module的更多信息,所以还必须给Module类也制作一个序列化器,编辑serializers.py,修改和增加下列代码:
from ..models import Module class ModuleSerializer(serializers.ModelSerializer): class Meta: model = Module fields = ['order','title','description'] class CourseSerializer(serializers.ModelSerializer): modules = ModuleSerializer(many=True, read_only=True) class Meta: model = Course fields = ['id', 'subject', 'title', 'slug', 'overview', 'created', 'owner', 'modules']
首先为Module类制作了一个序列化器,然后给 CourseSerializer 类增加了一个属性modules为Module类的序列化器,many=True表示需要序列化多个对象,read_only参数表示这个字段是只读的,不应该被包含在任何需要进行增删改的字段中。
重新启动Python shell,再执行一遍上边在Python shell中的代码,可以看到结果中关于modules的部分变成类似下列:
"modules": [ { "order": 0, "title": "Introduction to overview", "description": "A brief overview about the Web Framework." }, { "order": 1, "title": "Configuring Django", "description": "How to install Django." }, ... ]
这样就完成了嵌套序列化的工作,其本质就是在编写序列化器的时候,将与原来外键同名的字段名指定为对应类的序列化器即可。
关于序列化器的更多信息可以看REST框架文档。
建立自定义API视图
REST框架提供了一个APIView类,基于Django内置的View类基础上增加了RESTful API的功能,但与View类不同的是,APIView采用了REST框架自定义的处理Request和Response对象的方法,并且在返回HTTP响应的时候处理APIException错误,而且还包含内建的认证系统来管理对视图的访问。
下边通过APIView来建立一个视图供用户选课,编辑courses应用的api/views.py,增加如下代码:
from django.shortcuts import get_object_or_404 from rest_framework.views import APIView from rest_framework.response import Response from ..models import Course class CourseEnrollView(APIView): def post(self, request, pk, format=None): course = get_object_or_404(Course, pk=pk) course.students.add(request.user) return Response({'enrolled': True})
这个视图管理用户选课的功能。代码解释如下:
- 建立一个类继承APIView
- 在其中定义了post()方法用于处理POST请求,这个类不需要处理其他类型的HTTP请求。
- 这个类需要接收一个pk参数,为课程的主键id,用于取得该课程对象。如果找不到就返回404错误。
- 添加当前用户与该课程的多对多关系,即选课。
编辑api/urls.py,为新的视图加入一行:
path('courses/<pk>/enroll/', views.CourseEnrollView.as_view(), name='course_enroll'),
这样建立好之后,理论上我们就可以发送一个POST请求来选课,而无需再页面中进行选择。但是这样就需要区分用户身份,避免未认证的用户也来发送POST请求。来看看API认证与权限管理是如何工作的。
处理身份认证
REST框架提供了一个认证类,用于鉴别提交HTTP请求的用户身份。如果认证通过,REST框架会在 request.user中设置认证后的用户对象,如果没有用户通过认证,则request.user中被设置一个Django内置的AnonymousUser对象。
REST框架提供如下的认证后端:
- BasicAuthentication:这是基础的HTTP认证(BA认证),用户和密码存放在HTTP请求头的Authorization键,以Base64格式发送。关于BA认证的具体内容看这里。
- TokenAuthentication:这是基于token的认证,一个token数据类存放用户的token,HTTP请求头中的Authorization键中存储token数据用于验证。
- SessionAuthentication:使用Django的session后端进行验证,对于前端发来的AJAX请求需要采用该方式验证。
- RemoteUserAuthentication:允许使用web服务器代理认证,会在环境变量中设置一个REMOTE_USER变量。
除此之外,还可以继承REST框架提供的 BaseAuthentication 类并且重写authenticate()方法来建立自定义的验证后端。
通过DEFAULT_AUTHENTICATION_CLASSES还可以设置认证是基于每个视图的,还是全局认证。注意,认证(Authentication)只解决用户身份的问题,即识别发请求的用户身份,但不会允许或阻止用户访问视图,必须通过设置用户权限来限制访问视图。
关于REST框架的认证可以参考其文档。
在视图中增加 BasicAuthentication类(注意不是BaseAuthentication类),编辑api/views.py,为CourseEnrollView添加一行:
from rest_framework.authentication import BasicAuthentication class CourseEnrollView(APIView): authentication_classes = (BasicAuthentication,) # ......
现在视图就可以通过HTTP请求头的Authorization键中的信息进行用户身份认证了。
为视图增加权限控制
刚才已经说了,单独用户认证是没有用的,必须与权限管理结合起来。REST框架提供了一个权限系统用于控制对视图的访问。一些REST框架内建的权限有:
- AllowAny:完全开放权限,不管用户认证与否,都不做任何限制
- IsAuthenticated:仅允许通过认证的用户
- IsAuthenticatedOrReadOnly:认证用户具有完整权限,匿名用户只读(只能使用GET,HEAD,OPTIONS三种 HTTP 请求种类)。
- DjangoModelPermissions:使用django.contrib.auth的权限管理系统。视图需要一个queryset属性,只有认证的用户加上具备访问某个数据类的权限才能够进行操作(类似之前的Instructors用户组)
- DjangoObjectPermissions:也使用Django权限,但是是基于每个对象的单独权限设置。
如果用户因为权限问题操作失败,则通常会得到下列HTTP响应码和错误信息:
- HTTP 401: Unauthorized
- HTTP 403:Permission denied
可以在REST框架的权限文档中找到更多信息。
继续编辑api/views.py,为CourseEnrollView添加具体的权限:
from rest_framework.permissions import IsAuthenticated class CourseEnrollView(APIView): authentication_classes = (BasicAuthentication,) permission_classes = (IsAuthenticated,) # ......
在配置完用户认证后,又为这个类加上了只有认证用户可以进行操作的权限。现在可以尝试给这个视图对应的URL发一个POST请求。
启动站点,然后在命令行里输入下列命令:
curl -i -X POST http://127.0.0.1:8000/api/courses/1/enroll/
应该会得到下列响应:
HTTP/1.1 401 Unauthorized ...... {"detail":"Authentication credentials were not provided."}
结果得到了401响应,因为我们没有认证过。现在我们为请求头增加一个已经注册的用户的认证信息,将下列代码中的student:password替换成你网站中的实际用户名和密码,然后执行命令:
curl -i -X POST -u student:password http://127.0.0.1:8000/api/courses/1/enroll/
会得到如下响应:
HTTP/1.1 200 OK ...... {"enrolled":true}
现在可以通过站点或者检查数据库,看该用户是否已经选上了课。
建立视图集合路由
视图集允许对API定义一系列的交互动,允许REST框架使用一个Router对象动态的建立URL。通过使用视图集,可以避免重复编写视图逻辑。REST框架中的视图集涵盖的经典的增删改查动作,包括list(), create(), retrieve(), update(), partial_update(), 和 destroy().
为Course模型建立一个视图集,编辑api/views.py文件,增加如下代码:
from rest_framework import viewsets from .serializers import CourseSerializer class CourseViewSet(viewsets.ReadOnlyModelViewSet): queryset = Course.objects.all() serializer_class = CourseSerializer
建立一个类并继承了ReadOnlyModelViewSet,ReadOnlyModelViewSet类提供了只读的list()和retrieve()方法,可以返回一个对象集合或者单个对象。编辑 api/urls.py,为视图集配置URL:
from django.urls import path, include from rest_framework import routers from . import views router = routers.DefaultRouter() router.register('courses', views.CourseViewSet) urlpatterns = [ # ...... path('', include(router.urls)), ]
建立了一个默认的路由对象,然后将CourseViewSet视图注册到路由中,使用了前缀'courses',现在这个router对象就可以为视图集动态的生成URL。
打开 http://127.0.0.1:8000/api/ ,可以看到如下内容的页面:
这个时候可以访问 http://127.0.0.1:8000/api/courses/ ,就可以得到JSON格式的课程列表。这个路径中的/courses/就是注册路由的时候使用的前缀courses。
视图集的详细使用可以看 https://www.django-rest-framework.org/api-guide/viewsets/,路由的使用方法可以参考https://www.django-rest-framework.org/api-guide/routers/。
为视图集添加额外功能
除了像上一小节一样使用默认视图集仅修改几个设置,还可以为视图集添加额外功能。
让我们来把CourseEnrollView编程一个自定义的视图集功能。编辑api/views.py,修改CourseViewSet:
from rest_framework.decorators import detail_route class CourseViewSet(viewsets.ReadOnlyModelViewSet): queryset = Course.objects.all() serializer_class = CourseSerializer @detail_route(methods=['post'], authentication_classes=[BasicAuthentication], permission_classes=[IsAuthenticated]) def enroll(self, request, *args, **kwargs): course = self.get_object() course.students.add(request.user) return Response({'enrolled': True})
为视图集增加了一个自定义的方法 enroll(),代表为视图集增加的新功能,解释如下:
- 使用 detail_router 装饰器(该装饰器已经被Pycharm提醒要被删除,未来改用@action装饰器),定义了这是一个在单个对象上执行的功能。
- 这个装饰器同时还运行添加参数,methods设置为['post']表示该视图只接受post请求,然后还设置了验证和权限。
- 使用self.get_object()获取当前的Course对象。
- 把当前的用户增加到多对多关系中(选课)
然后编辑api/urls.py,去掉下边这一行,因为通过router动态配置了新的路由,这一行无需再用:
path('courses/<pk>/enroll/', views.CourseEnrollView.as_view(), name='course_enroll'),
然后编辑api/views.py,删除CourseEnrollView,因为这个类的功能现在成为视图集的一部分。
现在选课功能的URL由router自动生成,实际的URL与刚才相同,因为使用了我们自定义的函数名称 enroll。
自定义权限
我们希望只有选了某课程的学生用户才可以访问该课程的全部内容。最好的方式就是自定义一个权限,REST框架(原书有误,为Django)提供了一个 BasePermission 类允许重写下列方法:
- has_permission():视图级别的权限检查
- has_object_permission():对象级别的权限检查
这两个方法必须返回True表示允许权限或False表示不具有权限。在courses/api/目录下建立一个新文件permissions.py:
from rest_framework.permissions import BasePermission class IsEnrolled(BasePermission): def has_object_permission(self, request, view, obj): return obj.students.filter(id=request.user.id).exists()
这个IsEnrolled权限继承了BasePermission类然后重写了has_object_permission方法。由于这个方法是基于对象的,所以obj就是当前的课程。检查当前用户是否在已经选该课的所有用户里。之后就可以使用该权限了。
序列化课程内容
现在已经把主题,课程和章节都序列化了。还必须序列化内容。Content模型有一个通用外键关系,可以用于检索所有内容模型。而且我们还为所有内容模型添加了.render()方法。可以使用这些关系和方法,来实现序列化。
编辑api/serializers.py,添加下列代码:
from ..models import Content class ItemRelatedField(serializers.RelatedField): def to_representation(self, value): return value.render() class ContentSerializer(serializers.ModelSerializer): item = ItemRelatedField(read_only=True) class Meta: model = Content fields = ['order', 'item']
在这段代码里,定义了一个特别的字段,继承RelatedField字段,然后重写了to_representation()方法。然后定义了内容序列化器并且指定与原来通用外键同名的item字段为刚定义的特殊字段,然后指定模型为Content模型。
我们还需要另外一个用于Module模型的序列化器,其中嵌套这个Content序列化器;还需要改造Course序列化器以让其也包含相关输出,编辑api/serializers.py添加下列代码:
class ModuleWithContentsSerializer(serializers.ModelSerializer): contents = ContentSerializer(many=True) class Meta: model = Module fields = ['order', 'title', 'description', 'contents'] class CourseWithContentsSerializer(serializers.ModelSerializer): modules = ModuleWithContentsSerializer(many=True) class Meta: model = Course fields = ['id', 'subject', 'title', 'slug', 'overview', 'created', 'owner', 'modules']
这其实就是一层一层从内到外嵌套序列化器,由于已经定义了Content的序列化器,就建立一个外层的ModuleWithContent序列化器,其中设置contents字段为Content序列器,再往上一层的CourseWithContent也是如此,嵌套ModuleWithContent。
再建立一个视图,模仿刚才的retrieve()行为,但是采用新的序列化器,编辑api/views.py,给CourseViewSet类 增加下列代码:
from .permissions import IsEnrolled from .serializers import CourseWithContentsSerializer class CourseViewSet(viewsets.ReadOnlyModelViewSet): # ... @detail_route(methods=['get'], serializer_class=CourseWithContentsSerializer, authentication_classes=[BasicAuthentication], permission_classes=[IsAuthenticated, IsEnrolled]) def contents(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs)
解释如下:
- 使用detail_route来定义该方法是针对一个单独数据对象的
- 该方法只接受GET请求
- 使用了CourseWithContentsSerializer这个新的序列器
- 添加了用户认证,以及登录用户和自定义权限
- 采用ReadOnlyModelViewSet提供的retrieve()方法来返回Course对象
然后打开 http://127.0.0.1:8000/api/courses/1/contents/ 。如果你的当前用户选了URL参数id对应的课程,就可以看到课程,章节和内容嵌套渲染后的字符串以JSON的形式显示出来,类似下边这样:
{ "order": 0, "title": "Introduction to Django", "description": "Brief introduction to the Django Web Framework.", "contents": [ { "order": 0, "item": "<p>Meet Django. Django is a high-level Python Web framework...</p>" }, { "order": 1, "item": "\n<iframe width=\"480\" height=\"360\" src=\"http://www.youtube.com/embed/bgV39DlmZ2U?wmode=opaque frameborder=\"0\" allowfullscreen></iframe>\n" } ] }
现在我们就建立了一个简单的符合RESTful风格的API,让网站自动化向外部提供数据。REST框架还可以使用ModelViewSet来控制建立和编辑数据对象。关于REST框架中的主要内容在本章都涉及到了,如果对于框架特性还需要详细了解,可以参考REST框架的官方文档:https://www.django-rest-framework.org/。
总结
在本章,为其他程序自动化使用本网站的程序,建立了一套API,其他应用程序(如爬虫,自动化测试程序等)只要访问本站的特定URL,就可以得到以JSON格式返回的本站数据。通过这个方式,可以让其他程序与我们的站点进行交互。
本章的内容略显琐碎,是因为我们使用的REST框架是基于原来的架构从很多方面去建立一套新的界面。RESTful风格的网站实际上也应该在网站设计阶段就考虑好。RESTful本身也是一个相当大的话题,单纯讨论REST概念的书就有很多,还需要在实际建站中慢慢摸索。
下一章将讨论如何通过uWSGI和NGINX建立生产环境。你还会学到如何实现一个自定义的中间件以及建立自定义的管理命令。