快速實例
大致步驟
(1)創建表,數據遷移 (2)創建表序列化類BookSerializer class BookSerializer(serializers.HyperlinkedModelSerializer): class Meta: model=Book fields="__all__" (3)創建視圖類: class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer (4) 設計url: router.register(r'books', views.BookViewSet)
序列化
創建一個序列化類
簡單使用
開發我們的Web API的第一件事是為我們的Web API提供一種將代碼片段實例序列化和反序列化為諸如json
之類的表示形式的方式。我們可以通過聲明與Django forms非常相似的序列化器(serializers)來實現。
models部分:
from django.db import models # Create your models here. class Book(models.Model): title=models.CharField(max_length=32) price=models.IntegerField() pub_date=models.DateField() publish=models.ForeignKey("Publish") authors=models.ManyToManyField("Author") def __str__(self): return self.title class Publish(models.Model): name=models.CharField(max_length=32) email=models.EmailField() def __str__(self): return self.name class Author(models.Model): name=models.CharField(max_length=32) age=models.IntegerField() def __str__(self): return self.name
views部分:
from rest_framework.views import APIView from rest_framework.response import Response from .models import * from django.shortcuts import HttpResponse from django.core import serializers from rest_framework import serializers class BookSerializers(serializers.Serializer): title=serializers.CharField(max_length=32) price=serializers.IntegerField() pub_date=serializers.DateField() publish=serializers.CharField(source="publish.name") #authors=serializers.CharField(source="authors.all") authors=serializers.SerializerMethodField() def get_authors(self,obj): temp=[] for author in obj.authors.all(): temp.append(author.name) return temp class BookViewSet(APIView): def get(self,request,*args,**kwargs): book_list=Book.objects.all() # 序列化方式1: # from django.forms.models import model_to_dict # import json # data=[] # for obj in book_list: # data.append(model_to_dict(obj)) # print(data) # return HttpResponse("ok") # 序列化方式2: # data=serializers.serialize("json",book_list) # return HttpResponse(data) # 序列化方式3: bs=BookSerializers(book_list,many=True) # 當傳的是一個queryset時要加many=True,如果是一個單個對象則不用加
return Response(bs.data)
我們可以看到當我們拿到book_list后,有很多種方式來進行序列化,這里我們使用的是rest_framwork帶的serializers來進行的序列化,在返回時我們也用到了rest_framwork的Response
還有一點值得注意的時,我們之前在寫CBV時,繼承的django.views中的View類,而這里我們是從rest_framwork.views中導入的APIView,我們來看看這個類具體做了什么
首先,在url中我們可以看到
from django.conf.urls import url from django.contrib import admin from api import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^books/$', views.BookView.as_view()), ]
這里我們還是使用的as_view這個方法,那么在APIView中這個方法是如何定義的呢
@classmethod def as_view(cls, **initkwargs): """ Store the original class on the view function. This allows us to discover information about the view when we do URL reverse lookups. Used for breadcrumb generation. """ if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet): def force_evaluation(): raise RuntimeError( 'Do not evaluate the `.queryset` attribute directly, ' 'as the result will be cached and reused between requests. ' 'Use `.all()` or call `.get_queryset()` instead.' ) cls.queryset._fetch_all = force_evaluation view = super(APIView, cls).as_view(**initkwargs) view.cls = cls view.initkwargs = initkwargs # Note: session based authentication is explicitly CSRF validated, # all other authentication is CSRF exempt. return csrf_exempt(view)
可以看到這里其實是通過super方法執行的它父類的as_view方法,而它的父類正式django的View類,所以這里的view其實就是父類執行as_view方法獲得的view函數,在url中得到這個函數后,會自動去執行它,而執行它其實就是在執行dispatch方法
def view(request, *args, **kwargs): self = cls(**initkwargs) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs return self.dispatch(request, *args, **kwargs)
我們來看看APIView的dispatch方法都做了什么
def dispatch(self, request, *args, **kwargs): """ `.dispatch()` is pretty much the same as Django's regular dispatch, but with extra hooks for startup, finalize, and exception handling. """ self.args = args self.kwargs = kwargs request = self.initialize_request(request, *args, **kwargs) self.request = request self.headers = self.default_response_headers # deprecate? try: self.initial(request, *args, **kwargs) # Get the appropriate handler method if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed response = handler(request, *args, **kwargs) except Exception as exc: response = self.handle_exception(exc) self.response = self.finalize_response(request, response, *args, **kwargs) return self.response
這里的dispatch首先重新定義了一下request,request = self.initialize_request(request, *args, **kwargs),這個initialize_request方法的內容如下
def initialize_request(self, request, *args, **kwargs): """ Returns the initial request object. """ parser_context = self.get_parser_context(request) return Request( request, parsers=self.get_parsers(), authenticators=self.get_authenticators(), negotiator=self.get_content_negotiator(), parser_context=parser_context )
可以看到它其實就是返回了一個Request類的實例對象,那么這個類又做了什么呢
def __init__(self, request, parsers=None, authenticators=None, negotiator=None, parser_context=None): assert isinstance(request, HttpRequest), ( 'The `request` argument must be an instance of ' '`django.http.HttpRequest`, not `{}.{}`.' .format(request.__class__.__module__, request.__class__.__name__) ) self._request = request self.parsers = parsers or () self.authenticators = authenticators or () self.negotiator = negotiator or self._default_negotiator() self.parser_context = parser_context self._data = Empty self._files = Empty self._full_data = Empty self._content_type = Empty self._stream = Empty if self.parser_context is None: self.parser_context = {} self.parser_context['request'] = self self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET force_user = getattr(request, '_force_auth_user', None) force_token = getattr(request, '_force_auth_token', None) if force_user is not None or force_token is not None: forced_auth = ForcedAuthentication(force_user, force_token) self.authenticators = (forced_auth,)
別的暫時先不看,我們可以看到它定義了一個self._request = request,所以我們以后如果想使用wsgi封裝的request其實需要用request._request才行
在定義完新的request之后,dispatch又進行了一步初始化操作self.initial(request, *args, **kwargs),這個我們后面再說,然后進行的就和View類一樣,判斷請求的方式是否在self.http_method_names中,如果在就利用反射的方式取到,然后執行,但是這樣要注意的是,這時后執行get,post等方法時傳進去的request已經是重新定義過后的了
看完了源碼的大體流程,我們再來看看序列化的類干了什么
from rest_framework import serializers from rest_framework.response import Response from rest_framework.views import APIView class BookSerializers(serializers.Serializer): title = serializers.CharField(max_length=32) price = serializers.IntegerField() pub_date = serializers.DateField() publish = serializers.CharField(source="publish.id") # authors = serializers.CharField(source="authors.all") # 針對多對多 authors = serializers.SerializerMethodField() def get_authors(self, obj): temp = [] for author in obj.authors.all(): temp.append(author.pk) return temp
首先定義這個類其實和我們之前使用的form有點像,不過這里要注意一對多的字段,有個source屬性,可以控制我們具體顯示的內容,默認是顯示這個對象,這里我們可以定義為對象的id等,而多對多的字段我們可以自定義一個方法
serializers.SerializerMethodField()是固定寫法,而下面的函數名必須是get_字段名,函數的參數obj就是每一個book對象,這樣我們通過這個類,在使用postman進行get請求時就能得到如下的數據
[ { "id": 1, "authors": [ 1, 2 ], "title": "python", "price": 123, "pub_date": "2018-04-08", "publish": 1 }, { "id": 2, "authors": [ 1 ], "title": "go", "price": 222, "pub_date": "2018-04-08", "publish": 2 }, { "id": 3, "authors": [ 2 ], "title": "java", "price": 222, "pub_date": "2018-04-08", "publish": 2 } ]
上面的寫法跟普通的form類似,當然我們還學過ModelForm,這里也有類似的用法
class BookSerializers(serializers.ModelSerializer): class Meta: model = Book fields = "__all__" authors = serializers.SerializerMethodField() def get_authors(self, obj): temp = [] for author in obj.authors.all(): temp.append(author.pk) return temp
這里如果不單獨定義多對多的字段和一對多的字段,那么默認顯示的是字段的pk值
提交post請求
def post(self,request,*args,**kwargs): bs=BookSerializers(data=request.data,many=False) if bs.is_valid(): # print(bs.validated_data) bs.save() return Response(bs.data) else: return HttpResponse(bs.errors)
post請求是用來添加數據的,這里的request經過了新的封裝,我們可以從request.data中拿到數據,然后就像使用form表單一樣使用serializers,實例化一個對象,通過is_valid方法驗證數據,如果沒問題直接save保存數據,有問題的話則返回錯誤信息,這里要注意,當數據保存后我們要將新添加的數據返回(Json格式)
各種情況下需要返回的內容
全部信息 users/ ----查看所有數據 get users/ :返回所有的用戶的json數據 ----提交數據 post users/ :返回添加數據的json數據 具體對象 users/2 ----查看 get users/2 :返回具體查看的用戶的json數據 ----刪除 delete users/2 :返回空文檔 ----編輯 put/patch users/2:返回的編輯后的json數據包
單條數據的get、put和delete請求
這里需要對單條數據進行操作,所以要有一個新的url和新的視圖
from django.conf.urls import url from django.contrib import admin from api import views urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^books/$', views.BookView.as_view(), name="book_list"), url(r'^books/(\d+)/$', views.BookDetailView.as_view(), name="book_detail"), ]
新的url對應的新視圖
# 針對Book某一個數據操作 查看,編輯,刪除一本書 class BookDetailView(APIView): # 查看一本書 def get(self, request, pk, *args, **kwargs): obj = Book.objects.filter(pk=pk).first() if obj: bs = BookSerializers(obj) return Response(bs.data) else: return Response() # 編輯一本書 def put(self, request, pk, *args, **kwargs): obj = Book.objects.filter(pk=pk).first() bs = BookSerializers(data=request.data, instance=obj) if bs.is_valid(): bs.save() return Response(bs.data) else: return Response(bs.errors) # 刪除一本書 def delete(self, request, pk, *args, **kwargs): Book.objects.filter(pk=pk).delete() return Response()
查看時跟我們之前的區別在於,之前是序列化的一個queryset,現在只是一個單個對象,然后將這個對象的序列化結果返回,這里多加了一步判斷,如果這個對象不存在則返回空
編輯時,我們首先通過主鍵值取到這個書對象,然后像使用form組件一樣,將更新的數據和這個對象傳給我們的序列化類進行實例化,這里注意參數instance和form組件一樣,然后通過is_valid驗證數據是否正確,如果正確則save並返回這條記錄,錯誤則返回錯誤信息
刪除時,直接過濾出我們要刪除的進行刪除即可,返回空
超鏈接API:Hyperlinked
采用上面的內容,我們訪問時,外鍵內容我們可以看到的是主鍵值,但是如果我們想看到一個鏈接url呢,那么就要采用下面的方法
class BookSerializers(serializers.ModelSerializer): publish= serializers.HyperlinkedIdentityField( view_name='publish_detail', lookup_field="publish_id", lookup_url_kwarg="pk") class Meta: model=Book fields="__all__" #depth=1
這里我們重新定義了publish字段,view_name是url對應的別名,lookup_field則是url上對應的需要顯示的內容,lookup_url_kwarg是url上的分組名稱
urls部分:
urlpatterns = [ url(r'^books/$', views.BookViewSet.as_view(),name="book_list"), url(r'^books/(?P<pk>\d+)$', views.BookDetailViewSet.as_view(),name="book_detail"), url(r'^publishers/$', views.PublishViewSet.as_view(),name="publish_list"), url(r'^publishers/(?P<pk>\d+)$', views.PublishDetailViewSet.as_view(),name="publish_detail"), ]
這樣,我們訪問時就能看到publish的結果是一個url鏈接
視圖
使用混合(mixins)
使用我們上面的方法,如果我們要再定義一個publish表的操作,則需要再寫兩個視圖
from rest_framework.views import APIView from rest_framework.response import Response from .models import * from django.shortcuts import HttpResponse from django.core import serializers from rest_framework import serializers class BookSerializers(serializers.ModelSerializer): class Meta: model=Book fields="__all__" #depth=1 class PublshSerializers(serializers.ModelSerializer): class Meta: model=Publish fields="__all__" depth=1 class BookViewSet(APIView): def get(self,request,*args,**kwargs): book_list=Book.objects.all() bs=BookSerializers(book_list,many=True,context={'request': request}) return Response(bs.data) def post(self,request,*args,**kwargs): print(request.data) bs=BookSerializers(data=request.data,many=False) if bs.is_valid(): print(bs.validated_data) bs.save() return Response(bs.data) else: return HttpResponse(bs.errors) class BookDetailViewSet(APIView): def get(self,request,pk): book_obj=Book.objects.filter(pk=pk).first() bs=BookSerializers(book_obj,context={'request': request}) return Response(bs.data) def put(self,request,pk): book_obj=Book.objects.filter(pk=pk).first() bs=BookSerializers(book_obj,data=request.data,context={'request': request}) if bs.is_valid(): bs.save() return Response(bs.data) else: return HttpResponse(bs.errors) class PublishViewSet(APIView): def get(self,request,*args,**kwargs): publish_list=Publish.objects.all() bs=PublshSerializers(publish_list,many=True,context={'request': request}) return Response(bs.data) def post(self,request,*args,**kwargs): bs=PublshSerializers(data=request.data,many=False) if bs.is_valid(): # print(bs.validated_data) bs.save() return Response(bs.data) else: return HttpResponse(bs.errors) class PublishDetailViewSet(APIView): def get(self,request,pk): publish_obj=Publish.objects.filter(pk=pk).first() bs=PublshSerializers(publish_obj,context={'request': request}) return Response(bs.data) def put(self,request,pk): publish_obj=Publish.objects.filter(pk=pk).first() bs=PublshSerializers(publish_obj,data=request.data,context={'request': request}) if bs.is_valid(): bs.save() return Response(bs.data) else: return HttpResponse(bs.errors)
可以方法,代碼基本都是重復的,context={'request': request}是使用超鏈接API時需要加的內容
通過使用mixin類編寫視圖:
from rest_framework import mixins from rest_framework import generics class BookViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): queryset = Book.objects.all() serializer_class = BookSerializers def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) class BookDetailViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView): queryset = Book.objects.all() serializer_class = BookSerializers 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)
這里我們用到了多繼承,當執行對應的方法時,其實會一步一步的從每一個繼承的類中去找,這里我們以BookViewSet的get方法為例,當執行它時其實執行的是self.list(request, *args, **kwargs),這個list方法我們可以在它的第一個繼承類mixins.ListModelMixin中找到
這個方法的內容其實和我們自己寫的get方法是類似的,先拿到queryset,再利用序列化類進行實例化,最后返回序列化的結果
其它的方法也都跟這里類似,執行一個方法時只要按照繼承順序一步一步的找就行了
通過這種方式,我們就可以不用自己寫那么多代碼,而是直接使用繼承類的內容,但是如果要再寫一個publish表還是要寫很多重復的內容
使用通用的基於類的視圖
通過使用mixin類,我們使用更少的代碼重寫了這些視圖,但我們還可以再進一步。REST框架提供了一組已經混合好(mixed-in)的通用視圖,我們可以使用它來簡化我們的views.py
模塊
from rest_framework import mixins from rest_framework import generics class BookViewSet(generics.ListCreateAPIView): queryset = Book.objects.all() serializer_class = BookSerializers class BookDetailViewSet(generics.RetrieveUpdateDestroyAPIView): queryset = Book.objects.all() serializer_class = BookSerializers class PublishViewSet(generics.ListCreateAPIView): queryset = Publish.objects.all() serializer_class = PublshSerializers class PublishDetailViewSet(generics.RetrieveUpdateDestroyAPIView): queryset = Publish.objects.all() serializer_class = PublshSerializers
generics.ListCreateAPIView,我們可以點擊進去看看這個類的內容
可以看到它已經幫我們把get和post方法都封裝好了,其它的類也做了一樣的事情,所以我們在寫時,只需要繼承這些類,然后在我們自己類中定義好queryset和serializer_class這兩個參數即可,這樣重復的代碼就不用再寫了
再次優化
通過上面的方法我們已經將代碼優化了很多,那么還能不能再進行優化呢,其實還可以將我們定義的兩個類合並
from rest_framework.viewsets import ModelViewSet class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializers
這里我們繼承了一個新的類,可以看看這個類都有一些什么內容
可以看到其實這個類就是繼承了我們上面用到的各種類,這么用以后,為了加以區分,在url中我們需要多傳一點參數
views.BookViewSet.as_view({"get":"list","post":"create"}),name="book_list"), url(r'^books/(?P<pk>\d+)$', views.BookViewSet.as_view({ 'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy' }),name="book_detail"),
認證與權限組件
認證組件
局部視圖認證
為了做認證,我們先要有用戶相關的表,所以我們先添加表結構
from django.db import models # Create your models here. class Book(models.Model): title = models.CharField(max_length=32) price = models.IntegerField() pub_date = models.DateField() publish = models.ForeignKey("Publish") authors = models.ManyToManyField("Author") def __str__(self): return self.title class Publish(models.Model): name = models.CharField(max_length=32) email = models.EmailField() def __str__(self): return self.name class Author(models.Model): name = models.CharField(max_length=32) age = models.IntegerField() def __str__(self): return self.name class User(models.Model): user = models.CharField(max_length=32) pwd = models.CharField(max_length=32) user_type = models.IntegerField(choices=((1, "普通用戶"), (2, "VIP"), (3, "SVIP")), default=1) class UserToken(models.Model): user = models.OneToOneField("User") token = models.CharField(max_length=128)
這里我們添加了兩個表,一個用戶表還有一個用戶token表,token表中的token信息就是我們用來認證的字符串
表建好后,我們就要來創建login的邏輯了,這里的login我們只需要創建post請求的邏輯
from django.http import JsonResponse def get_random_str(user): import hashlib, time ctime = str(time.time()) md5 = hashlib.md5(bytes(user, encoding="utf8")) md5.update(bytes(ctime, encoding="utf8")) return md5.hexdigest() class LoginView(APIView): def post(self, request, *args, **kwargs): user = request.data.get("user") pwd = request.data.get("pwd") token = request.data.get("token") user = User.objects.filter(user=user, pwd=pwd).first() res = {"state_code": 200, "msg": None} if user: random_str = get_random_str(user.user) UserToken.objects.update_or_create(user=user, defaults={"token": random_str}) res["msg"] = "success" res["token"] = random_str else: res["msg"] = "用戶名或密碼錯誤" res["state_code"] = 110 return JsonResponse(res)
當用戶登錄后我們先要判斷用戶名和密碼是否正確,如果正確的話,我們通過md5生成一串隨機字符串,然后將這個隨機字符串添加到usertoken表中,這里要注意的是我們用到了update_or_create,當user=user的數據存在時就更新,否則就添加
如果登錄不成功,則返回錯誤信息
用戶登錄完成后,我們就開始做認證了,首先我們需要創建一個認證類
from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication class Authentication(BaseAuthentication): def authenticate(self, request): token = request._request.GET.get("token") user_token_obj = UserToken.objects.filter(token=token).first() if user_token_obj: return user_token_obj.user, token else: raise exceptions.AuthenticationFailed("token驗證失敗")
類名隨便取,但是類中必須有一個authenticate方法,在這個方法中做我們的邏輯判斷,這里我們就從url上拿token值到數據庫中搜索,如果有表示登錄成功,這是需要返回一個元組,元組的兩個值可以根據需要返回,如果不成功則要拋出一個異常
類定義完后我們就需要在我們的視圖類中添加一個屬性
class BookViewSet(ModelViewSet): authentication_classes = [Authentication] queryset = Book.objects.all() serializer_class = BookSerializers
這樣,當用戶訪問Book表相關功能時就會進行認證了
全局視圖認證組件
上面我們介紹了局部認證,那么如果想讓全局都進行認證呢,只需要在settings配置中增加
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",] }
字典中的內容為認證類的路徑
源碼分析
上面我們介紹了認證組件的用法,那么為什么這么用呢,這就要通過源碼來進行分析了,首先當用戶來訪問時,url匹配到了我們先執行的是一個as_view方法,這里我們從BookViewSet類繼承的ModelViewSet類開始一步步往上找
首先我們來看看這些類的繼承順序
然后再一步步找
ModelViewSet中沒有as_view
GenericViewSet中也沒有,generics.GenericAPIView中也沒有,最后在generics.GenericAPIView的父類APIView中發現as_view方法,這個方法在上面我們已經提到了,他返回了一個view函數,當匹配到url后,會執行這個view函數並給它傳一個request
而執行這個view其實就是在self.dispatch方法,我們還是一步步找,最后還是在APIView找到了,這個方法我們上面說到了他重新封裝了request,然后又進行了初始化操作
現在我們來看看初始化操作self.initial方法做了什么
上面的操作我們先不管,我們發現這里有一步關於認證的操作self.perform_authentication(request),這里到底做了什么呢
我們發現這里只有一個request.user,我們猜想這是一個靜態方法,這時我們就要去重新定義request的類中找這個方法了
可以看到這里其實就是執行了self._authenticate()方法
在這個方法中我們看到在for循環self.authenticators,那么這個self.authenticators是什么呢,我們注意到在Request類的初始化方法__init__中我們對它進行了定義
那么我們在實例化的過程中傳了什么呢
這里我們可以看到這里調用了self.get_authenticators()的方法
這個方法其實就是放回了一個列表,這個列表解析式中我們發現self.authentication_classes就是我們在局部認證時加在我們的視圖類中的內容,是一個列表中放着認證類
所以這里返回的列表中放着的其實就是我們的認證類對象
知道了這個內容后,我們在上圖所示的方法中其實就是在循環認證類對象的列表,然后調用對象的authenticate方法,所以我們在創建認證類時必須要有這個方法,而這個方法返回的是一個元組,在下面分別給self.user和self.auth賦值這個元組中的值
到這里我們就明白了為什么局部認證時要在視圖類中設置authentication_classes,並且認證類中一定要有authenticate方法了,那么全局認證時又是為什么呢
我們發現,如果我們在視圖類中沒有定義authentication_classes屬性,那么在找authentication_classes時,我們會在APIView中找到
這里的api_settings是一個實例化的對象
在這個類中都有什么內容呢
首先我們發現,如果我們沒有傳值,這個defaults會有一個默認的內容DEFAULTS
這就是我們不做任何配置時系統自動進行的認證內容,但是這是個字典的內容,我們為什么能直接點出它的內容呢,我們發現在這個類中有個__getter__方法
在這個方法中,我們發現他通過val = self.defaults[attr]直接取值了,那么上面還有一個val = self.user_settings[attr],當它取值出錯時才會去默認的defaults中取值
那么這個user_settings是什么呢
可以看到它就是我們在settings中配置的REST_FRAMEWORK,所以當我們要做全局認證時只要在settings中配置REST_FRAMEWORK並且里面的鍵叫做DEFAULT_AUTHENTICATION_CLASSES即可
權限組件
局部視圖權限
from rest_framework.permissions import BasePermission class SVIPPermission(BasePermission): message="SVIP才能訪問!" def has_permission(self, request, view): if request.user.user_type==3: return True return False
權限組件的原理與上面的認證組件類似,這里就不再贅述,可以參考源碼
可以看到也是先定義一個權限類,類中必須有has_permission方法,這個方法返回Ture或者False
from app01.service.permissions import * class BookViewSet(generics.ListCreateAPIView): permission_classes = [SVIPPermission,] queryset = Book.objects.all() serializer_class = BookSerializers
在視圖類中同樣是加一個參數permission_classes
全局視圖權限
在settings中配置
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",] }
hrottle(訪問頻率)組件
局部視圖throttle
和認證、權限組件一樣,首先我們要定義一個頻率類
from rest_framework.throttling import BaseThrottle,SimpleRateThrottle import time VISITED_RECORD={} class VisitThrottle(BaseThrottle): def __init__(self): self.history=None def allow_request(self,request,view): print("ident",self.get_ident(request)) #visit_ip=request.META.get('REMOTE_ADDR') visit_ip=self.get_ident(request) print(visit_ip) ctime=time.time() #第一次訪問請求 if visit_ip not in VISITED_RECORD: VISITED_RECORD[visit_ip]=[ctime] return True # self.history:當前請求IP的記錄列表 self.history = VISITED_RECORD[visit_ip] print(self.history) # 第2,3次訪問請求 if len(VISITED_RECORD[visit_ip])<3: VISITED_RECORD[visit_ip].insert(0,ctime) return True if ctime-VISITED_RECORD[visit_ip][-1]>60: VISITED_RECORD[visit_ip].pop() VISITED_RECORD[visit_ip].insert(0,ctime) print("ok") return True return False def wait(self): import time ctime = time.time() return 60 - (ctime - self.history[-1])
這個類中必須有一個allow_request方法和wait方法,期中allow_request中放我們的邏輯,這里我們限制同一個ip每分鍾只能進行3次訪問,如果超過了就返回False,不超過在返回True
在邏輯中我們先定義了一個空的字典,當一個ip來訪問時,我們先判斷這個ip在不在字典中,如果不在,說明是第一次訪問,那么在字典中添加一個鍵值對,鍵為這個ip,值為當前訪問時間戳的列表
當這個ip第2次或第3次來訪問時,由於列表中的時間戳小於3個,所以直接將訪問的時間戳插入進去就行了,第4次訪問時,我們就拿訪問的時間戳減去列表中的最后一個值,如果大於60,說明當前這次訪問與第一次訪問的間隔超過了1分鍾,沒有超過我們的訪問頻率要求,所以我們將新的時間戳加入列表,並將原來列表最后的值刪掉(保證列表中最多只有3個值),如果結果小於60則表示訪問頻率過高,返回False限制訪問
wait函數的返回值就是我們還有多少秒能再次訪問,定義好這個類后,在我們視圖類中像使用
class BookViewSet(ModelViewSet): authentication_classes = [MyAuthentication,] permission_classes = [SVIPPermission] throttle_classes = [VisitThrottle] queryset = Book.objects.all() serializer_class = BookSerializers
這里我們用到的是throttle_classes參數,這樣book表就能享受到訪問頻率限制了
全局視圖throttle
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",], "DEFAULT_THROTTLE_CLASSES":["app01.service.throttles.VisitThrottle",] }
和上面兩個組件一樣,在settings中配置DEFAULT_THROTTLE_CLASSES
內置throttle類
BaseThrottle,在上面的局部使用中,我們可以看到我們繼承了BaseThrottle類,這個類中有幾個方法
可以看到里面已經有了我們用到的allow_request和wait方法,所以我們可以不寫wait,但是allow_request要求要被覆蓋,這里還有一個get_ident方法,通過這個方法我們可以直接獲取訪問的ip地址
SimpleRateThrottle
class VisitThrottle(SimpleRateThrottle): scope="visit_rate" def get_cache_key(self, request, view): return self.get_ident(request)
繼承這個類后,我們在其中定義一個scope屬性,通過這個屬性的值我們可以在settings中配置訪問頻率,還要定義一個get_cache_key方法,返回訪問的ip地址
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",], "DEFAULT_THROTTLE_CLASSES":["app01.service.throttles.VisitThrottle",], "DEFAULT_THROTTLE_RATES":{ "visit_rate":"5/m", } }
在settings中配置DEFAULT_THROTTLE_RATES
解析器
request類
django的request類和rest-framework的request類的源碼解析
django的request對象是通過from django.core.handlers.wsgi import WSGIRequest類實例化來的,通過它我們可以找到許多我們在request中使用的方法
而rest-framework中對request又進行了封裝
在使用django的request對象時,如果數據是以application/x-www-form-urlencoded格式發來的,那么我們可以在request.POST和request.GET中拿到數據,如果是別的形式的數據,那么我們能從request.body中拿到源數據
而在rest-framework的request中,不論數據什么格式,我們都是從request.data中拿數據,其實rest-framework的request中有好幾個解析器,分別解析不同格式的數據,如果我們要定義一個解析器,可以在視圖類中加上一個參數
from rest_framework.parsers import JSONParser,FormParser class PublishViewSet(generics.ListCreateAPIView): parser_classes = [FormParser,JSONParser] queryset = Publish.objects.all() serializer_class = PublshSerializers def post(self, request, *args, **kwargs): print("request.data",request.data) return self.create(request, *args, **kwargs)
parser_classes參數就是來定義我們使用的解析器的,我們來看看rest-framework中都自帶了哪些解析器
可以看到默認情況下,有3個解析器,分別解析json、formdata和另外形式的數據,基本已經夠我們使用了,一般情況下我們是不需要再自己定義的
我們可以使用的一共有這么幾個解析器from rest_framework.parsers import JSONParser,FormParser,FileUploadParser,MultiPartParser
如果要在全局配置中定義使用的解析器,可以在settings中配置
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",], "DEFAULT_THROTTLE_CLASSES":["app01.service.throttles.VisitThrottle",], "DEFAULT_THROTTLE_RATES":{ "visit_rate":"5/m", }, "DEFAULT_PARSER_CLASSES":['rest_framework.parsers.FormParser',] }
分頁
簡單分頁
from rest_framework.pagination import PageNumberPagination,LimitOffsetPagination class PNPagination(PageNumberPagination): page_size = 1 # 每一頁顯示的數量 page_query_param = 'page' # url上的分頁參數 page_size_query_param = "size" # 在url上可以通過這個參數來調整每一頁顯示的數量 max_page_size = 5 # 每一頁上最多顯示的數量,不論上面設置的size定多少都不會超過這個 class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializers def list(self,request,*args,**kwargs): book_list=Book.objects.all() pp=LimitOffsetPagination() pager_books=pp.paginate_queryset(queryset=book_list,request=request,view=self) print(pager_books) bs=BookSerializers(pager_books,many=True) #return Response(bs.data) return pp.get_paginated_response(bs.data)
這里我們使用了分頁類PageNumberPagination,我們可以直接使用它來實例化,也可以寫一個類來繼承它,這樣可以重新定義一些參數
我們在視圖類中重新寫了list方法,先拿到book_list,然后通過分頁類實例化一個對象,然后通過這個對象的paginate_queryset方法獲得新的數據類型,利用序列化類進行序列化
在return時再利用get_paginated_response方法返回更多的信息
可以看到返回的數據上多出了上一頁和下一頁的鏈接
上面的方法我們需要重寫我們的list等方法才能實現分頁,其實我們也可以像用其它組件一樣,在視圖類中加一個參數來實現局部的分頁
class BookViewSet(ModelViewSet): renderer_classes = [JSONRenderer,BrowsableAPIRenderer] authentication_classes = [MyAuthentication,] permission_classes = [SVIPPermission] throttle_classes = [VisitThrottle] queryset = Book.objects.all() serializer_class = BookSerializers pagination_class = MyPageNumberPagination
pagination_class參數就是我們要定義的
偏移分頁
from rest_framework.pagination import PageNumberPagination,LimitOffsetPagination,CursorPagination class MyPageNumberPagination(LimitOffsetPagination):pass
繼承了這個類后我們來看看有什么參數
offset是開始時的索引,limit是顯示幾條,這樣就能決定在頁面上顯示多少信息
CursorPagination
class MyPageNumberPagination(CursorPagination): cursor_query_param="page" page_size=2 ordering="id"
繼承這個類后,我們在看到頁面上數據的上一頁和下一頁的信息時,會對頁碼進行加密
路由
在前面的代碼中路由都是我們自己寫的,其實rest-framework已經幫我們做了封裝,我們只要引用就可以自動生成路由了
from django.conf.urls import url,include from django.contrib import admin from api import views from rest_framework import routers router = routers.DefaultRouter() router.register(r'books', views.BookViewSet) urlpatterns = [ url(r'^admin/', admin.site.urls), # url(r"books\.(?P<format>\w+)$",views.BookViewSet.as_view({"get":"list","post":"create"}),name="book_list"), # url(r"books/$",views.BookViewSet.as_view({"get":"list","post":"create"}),name="book_list"), # url(r"books/(?P<pk>\d+)/$",views.BookViewSet.as_view({"get":"retrieve","delete":"destroy","put":"update"}),name="book_detail"), # url(r"books/(?P<pk>\d+)\.(?P<format>\w+)$",views.BookViewSet.as_view({"get":"retrieve","delete":"destroy","put":"update"}),name="book_detail"), url(r'^', include(router.urls)), url(r"publishes/$",views.PublishView.as_view(),name="publish_list"), url(r"publishes/(?P<pk>\d+)/$",views.PublishDetailView.as_view(),name="publish_detail"), url("login/$",views.LoginView.as_view()) ]
這里導入routers,然后通過DefaultRouter實例化一個對象,再通過register注冊,最后在url中寫上url(r'^', include(router.urls)),就會自動幫我們生成books相關的路由了,一共4條,就是上面注釋的內容
響應器
我們可以發現,當我們用瀏覽器訪問時是會返回頁面的(必須在settings中注冊rest-framework,否則會報錯),而用postman等工具返回時只會返回json數據,這是為什么呢
瀏覽器訪問
postman訪問
其實這是由響應器決定的
from api.service.page import * from rest_framework.renderers import BrowsableAPIRenderer,JSONRenderer class BookViewSet(ModelViewSet): renderer_classes = [JSONRenderer,BrowsableAPIRenderer] authentication_classes = [MyAuthentication,] permission_classes = [SVIPPermission] #throttle_classes = [VisitThrottle] queryset = Book.objects.all() serializer_class = BookSerializers pagination_class = MyPageNumberPagination # parser_classes = [JSONParser]
這里renderer_classes參數定義的就是響應器,其中JSONRenderer是用來響應postman等工具的,響應結果就是json數據,而BrowsableAPIRenderer響應的瀏覽器頁面,如果想要改變響應的形式只要修改這個參數即可
版本
我們有很多種方式來訪問不同版本的api
基於url的get傳參方式
如:/users?version=v1
在settings中我們可以做相關配置
REST_FRAMEWORK = { 'DEFAULT_VERSION': 'v1', # 默認版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允許的版本 'VERSION_PARAM': 'version' # URL中獲取值的key }
url如下所示
from django.conf.urls import url, include from web.views import TestView urlpatterns = [ url(r'^test/', TestView.as_view(),name='test'), ]
在視圖中我們可以從request.version中取到用戶當前訪問的版本,還可以從request.versioning_scheme中取到版本管理的類,這種方法中我們使用的versioning_class是QueryParameterVersioning
#!/usr/bin/env python # -*- coding:utf-8 -*- from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.versioning import QueryParameterVersioning class TestView(APIView): versioning_class = QueryParameterVersioning def get(self, request, *args, **kwargs): # 獲取版本 print(request.version) # 獲取版本管理的類 print(request.versioning_scheme) # 反向生成URL reverse_url = request.versioning_scheme.reverse('test', request=request) print(reverse_url) return Response('GET請求,響應內容') def post(self, request, *args, **kwargs): return Response('POST請求,響應內容') def put(self, request, *args, **kwargs): return Response('PUT請求,響應內容')
基於url的正則方式
如:/v1/users/
settings
REST_FRAMEWORK = { 'DEFAULT_VERSION': 'v1', # 默認版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允許的版本 'VERSION_PARAM': 'version' # URL中獲取值的key }
url
from django.conf.urls import url, include from web.views import TestView urlpatterns = [ url(r'^(?P<version>[v1|v2]+)/test/', TestView.as_view(), name='test'), ]
視圖
#!/usr/bin/env python # -*- coding:utf-8 -*- from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.versioning import URLPathVersioning class TestView(APIView): versioning_class = URLPathVersioning def get(self, request, *args, **kwargs): # 獲取版本 print(request.version) # 獲取版本管理的類 print(request.versioning_scheme) # 反向生成URL reverse_url = request.versioning_scheme.reverse('test', request=request) print(reverse_url) return Response('GET請求,響應內容') def post(self, request, *args, **kwargs): return Response('POST請求,響應內容') def put(self, request, *args, **kwargs): return Response('PUT請求,響應內容')
基於 accept 請求頭方式
如:Accept: application/json; version=1.0
settings
REST_FRAMEWORK = { 'DEFAULT_VERSION': 'v1', # 默認版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允許的版本 'VERSION_PARAM': 'version' # URL中獲取值的key }
url
from django.conf.urls import url, include from web.views import TestView urlpatterns = [ url(r'^test/', TestView.as_view(), name='test'), ]
視圖
#!/usr/bin/env python # -*- coding:utf-8 -*- from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.versioning import AcceptHeaderVersioning class TestView(APIView): versioning_class = AcceptHeaderVersioning def get(self, request, *args, **kwargs): # 獲取版本 HTTP_ACCEPT頭 print(request.version) # 獲取版本管理的類 print(request.versioning_scheme) # 反向生成URL reverse_url = request.versioning_scheme.reverse('test', request=request) print(reverse_url) return Response('GET請求,響應內容') def post(self, request, *args, **kwargs): return Response('POST請求,響應內容') def put(self, request, *args, **kwargs): return Response('PUT請求,響應內容')
基於主機名方法
如:v1.example.com
ALLOWED_HOSTS = ['*'] REST_FRAMEWORK = { 'DEFAULT_VERSION': 'v1', # 默認版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允許的版本 'VERSION_PARAM': 'version' # URL中獲取值的key }
url
from django.conf.urls import url, include from web.views import TestView urlpatterns = [ url(r'^test/', TestView.as_view(), name='test'), ]
視圖
#!/usr/bin/env python # -*- coding:utf-8 -*- from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.versioning import HostNameVersioning class TestView(APIView): versioning_class = HostNameVersioning def get(self, request, *args, **kwargs): # 獲取版本 print(request.version) # 獲取版本管理的類 print(request.versioning_scheme) # 反向生成URL reverse_url = request.versioning_scheme.reverse('test', request=request) print(reverse_url) return Response('GET請求,響應內容') def post(self, request, *args, **kwargs): return Response('POST請求,響應內容') def put(self, request, *args, **kwargs): return Response('PUT請求,響應內容')
基於django路由系統的namespace
如:example.com/v1/users/
REST_FRAMEWORK = { 'DEFAULT_VERSION': 'v1', # 默認版本 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允許的版本 'VERSION_PARAM': 'version' # URL中獲取值的key }
url
from django.conf.urls import url, include from web.views import TestView urlpatterns = [ url(r'^v1/', ([ url(r'test/', TestView.as_view(), name='test'), ], None, 'v1')), url(r'^v2/', ([ url(r'test/', TestView.as_view(), name='test'), ], None, 'v2')), ]
視圖
#!/usr/bin/env python # -*- coding:utf-8 -*- from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.versioning import NamespaceVersioning class TestView(APIView): versioning_class = NamespaceVersioning def get(self, request, *args, **kwargs): # 獲取版本 print(request.version) # 獲取版本管理的類 print(request.versioning_scheme) # 反向生成URL reverse_url = request.versioning_scheme.reverse('test', request=request) print(reverse_url) return Response('GET請求,響應內容') def post(self, request, *args, **kwargs): return Response('POST請求,響應內容') def put(self, request, *args, **kwargs): return Response('PUT請求,響應內容')
源碼分析
通過上面的各種方法,我們可以看出不管我們使用什么樣的versioning_class,最終在視圖中我們都可以從request.version中取到用戶訪問的版本,這是為什么呢,我們需要到源碼中找原因,我們知道當用戶訪問某個視圖時其實都是在執行APIView中的dispatch方法
我們來看看這個方法中做了什么
這個方法中我們前面提到了會執行一步初始化操作,我們的認證、權限等組件的內容都是在這里執行的,其實在這個初始化方法中還有跟版本有關的內容
可以看到這里用version和scheme兩個參數接收了self.determine_version方法的結果,可以知道這個方法返回的應該是一個元組,最后又把這兩個參數賦給了request.version和request.versioning_scheme,所以我們總是能在視圖中用request.version取到版本號
我們再來看看self.determine_version方法的內容
首先我們可以看到scheme版本類的值就是self.versioning_class()中拿到的,這里的默認值和其它組件一樣,如果我們自己定義了就用我們自己定義的,而版本則是通過scheme.determine_version方法獲取,不同的版本類獲取方法不同,但最終都會將獲得的版本號賦值
給request.version