一小時完成後台開發:DjangoRestFramework開發實踐

DjangoRestFramework開發實踐

在這之前我寫過一篇關於Django與Drf快速開發實踐的博客,Django快速開發實踐:Drf框架和xadmin配置指北,粗略說了一下Drf配置和基本使用,不過裏面只是涉及到最基本的CRUD,在正常的後端開發中涉及的諸如認證和權限、消息隊列、緩存之類的操作,上一篇博客並沒有涉及,這次開發我仔細了看了官方文檔的這幾個部分,把這部分的功能完善了起來。

Drf的設計很有Django的味道,(畢竟就是伴生框架嘛)封裝了很多功能,稍微配置一下就可以用,這點在快速開發方面真的好評。

開始進入正題。

認證和授權

任何系統都離不開認證和授權,Drf內置一套強大的鑒權系統,框架提供了幾種基本的認證和權限控制方式,小型系統基本夠用,還可以自定義權限中間件,很方便就能實現節流這樣的功能。

API認證

Drf內置的四種API認證方式,基本信息我做了一個表格:

認證方式 說明
BasicAuthentication 每次提交請求的時候附加用戶名和密碼來進行認證
TokenAuthentication 每次提交請求的時候在HTTP headers里附加Token進行認證
SessionAuthentication 用戶登錄之後系統在cookies存入sessionid進行認證
RemoteUserAuthentication 通過web服務器認證(apache/nginx這些)

我選擇的是基於Token的認證,客戶端登錄之後維護一個token,每次請求附加到HTTP headers,還算是方便。

Drf還可以自定義認證方式,只要繼承authentication.BaseAuthentication這個類然後重寫authenticate方法就好了。這裡只簡單說下步驟,具體的參考官方文檔。

  • 創建認證類:繼承BaseAuthentication、重寫authenticate方法
  • authenticate()返回值
    1. None:當前認證不管,等下一個認證來執行
    2. raise exceptions.AuthenticationFailed('用戶認證失敗')
    3. 有返回值元組形式:(元素1,元素2)元素1複製給request.user、元素2複製給request.auth

settings.py中可以配置默認的認證方式,這裡我添加了三個:

REST_FRAMEWORK = {
    # 身份驗證
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

當然也支持各種第三方的認證框架,比如下面這些:

使用這些認證方式,認證通過後,在views裏面request.user就是一個Django用戶對象,如果未認證,就是一個AnonymousUser對象。

接下來說說權限

API授權

Drf的接口權限有以下幾種:

一般來說小網站用到DjangoModelPermissions就是夠用的,或者乾脆簡單一點,用IsAuthenticatedqueryset限定請求的數據即可。

介紹完了基本概念,來看看代碼中是如何操作的。

在viewset中的使用

對於操作用戶信息的viewset,我只用了permissions.IsAuthenticated這個權限,然後覆蓋了ReadOnlyModelViewSetget_queryset方法,把queryset變成只包括當前用戶,這樣就保證了一個用戶只能操作自己的信息。

from rest_framework import authentication, permissions, viewsets

class UserViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = UserSerializer

    def get_queryset(self):
        return User.objects.filter(pk=self.request.user.pk)

viewset的action同樣可以使用權限,加在裝飾器的參數上即可:

@action(detail=True, methods=['GET'], permission_classes=[permissions.IsAuthenticated])
def some_actions(self, request, pk=None):
    dosomething
    return Response(SomeSerializer(some_data, many=True).data)

這裡提一下裝飾器的detail參數,這個代表了是對列表操作還是對單個對象操作,True就是對單個對象。

從請求的URL上應該可以很好理解。

  • 對列表://hostname/viewset/some_action/
  • 對單個對象://hostname/viewset/1/some_action/

這部分的參考資料(優先閱讀官方文檔):

ApiView和ViewSet

Drf的ApiView相當於Django的View,每個ViewSet是一組Restful的接口,看看viewset的代碼,其中定義了6個action,對應get、post、put、delete這些http方法。

class UserViewSet(viewsets.ViewSet):
    def list(self, request):
        pass

    def create(self, request):
        pass

    def retrieve(self, request, pk=None):
        pass

    def update(self, request, pk=None):
        pass

    def partial_update(self, request, pk=None):
        pass

    def destroy(self, request, pk=None):
        pass

除了自帶的這些,還可以定義額外的action,這個在前文已經提過了。

ApiView的話,使用起來和Django的差不多,一個apiview只能對應一個url,通過重寫getpost這些方法可以實現不同的http方法響應。

ApiView和ViewSet同樣通過在類字段中加入authentication_classespermission_classes實現認證和授權。

分頁 PAGINATION

Drf和Django一樣自帶分頁功能,很好用(當然也支持使用第三方的分頁功能)。

首先進行配置(不配置的話使用默認配置),這裡我設置每頁顯示十條記錄:

REST_FRAMEWORK = {
    # 設置分頁
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

使用得最多的ModelViewSet已經自帶分頁了,這個我們不用操心,不過如果自己定義了action來返回列表數據的話,就沒有分頁,這時候要用paginate_queryset方法來處理。

代碼如下:

@action(detail=False)
def tag(self, request):
    queryset = SomeModel.objects.all().order_by('-add_time')
    page = self.paginate_queryset(queryset)
    if page is not None:
    	return self.get_paginated_response(self.get_serializer(page, many=True).data)
    return Response(self.get_serializer(queryset, many=True).data)

可以看出Drf自動處理了不同頁面的請求,不用像Django一樣自己從GET或者POST數據里讀取page,分頁相關的方法直接在viewset對象裏面,非常方便。

API文檔設置

Drf支持很多文檔插件,這裡我用一下比較傳統的coreapi文檔,首先配置:

REST_FRAMEWORK = {
    # 文檔
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}

在使用之前還需要使用pip安裝coreapi:

pip install coreapi

所有的ViewSet只要用了Router註冊的話都默認添加到文檔里,不過ApiView的話是要自己添加的。

from rest_framework.schemas.coreapi import AutoSchema

class SomeView(APIView):
    schema = AutoSchema()

    def post(self, request):
        data = dosomething()
        return Response(data)

這樣就可以了,別忘了註冊路由,路由我在下文統一介紹。

配置完成之後文檔的效果應該是下圖這樣,這個頁面可以測試請求,還可以以各種方式登錄認證,比Drf默認的界面豐富一些~

路由

使用Drf框架可以有兩種路由方式,一種是Drf的路由,一直是Django的路由,兩種搭配使用。

Drf的Router

from rest_framework import routers

router = routers.DefaultRouter()
router.register('users', UserViewSet, basename='api-users')
router.register('user-profiles', UserProfileViewSet, basename='api-user-profiles')
router.register('tags', TagViewSet, basename='api-tags')
router.register('categories', CategoryViewSet, basename='api-categories')
router.register('articles', ArticleViewSet, basename='api-articles')

定於完成之後要添加到Django的urlpatterns裏面。

urlpatterns = [
    path('api/', include(router.urls)),
]

對於ApiView,路由和Django的ClassBaseView差不多:

urlpatterns = [
    path('login/', views.LoginView.as_view()),
    path('signup/', views.SignUpView.as_view()),
]

配置認證和文檔相關url

from rest_framework.authtoken import views as authtoken_view
from rest_framework.documentation import include_docs_urls

urlpatterns = [
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('api-token-auth/', authtoken_view.obtain_auth_token),
    path('api-docs/', include_docs_urls(title='One Cat Docs')),
]

Serializers

序列化器也是Drf的一個重要概念,和Django的Form很像,作用是將Model對象的數據轉換為json、xml之類的結構化數據。

使用起來很簡單,我一般都是這麼定義

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'

使用__all__直接把所有模型字段都包括進去,針對外鍵等關係字段,我們需要做一些其他處理。

這裡簡單介紹一下我常用的幾種關係字段處理方式

  • StringRelatedField:將關係字段顯示為一個字符串,這個字符串取決於該模型定義的__str__方法
  • PrimaryKeyRelatedField:顯示成該關係字段的主鍵,這個也是默認的
  • 嵌套序列化:定義一個該字段對應類型的序列化器,嵌套序列化

具體的使用方式可以在官網找到~

下面再介紹幾個常用的參數:

  • read_only:一般針對不想被修改的字段可以使用,比如說用戶id
  • many:用於多對多關係或者一對多,會將所有引用序列化為一個列表

限流

其實就是一個自定義的認證過程。

Drf內置有BaseThrottle SimpleRateThrottle,後者是前者的之類。

  • BaseThrottle 需要自己寫allow_requestwait方法,控制粒度更細
  • SimpleRateThrottle重寫get_cache_key和設置scope名稱就可以,更簡單

實現1分鐘內只能訪問3次的限流

SimpleRateThrottle代碼如下:

from rest_framework.throttling import SimpleRateThrottle

class VisitThrottle(SimpleRateThrottle):
    '''匿名用戶60s只能訪問三次(根據ip)'''
    scope = 'throttle'   #這裏面的值,自己隨便定義,settings裏面根據這個值配置throttle

    def get_cache_key(self, request, view):
        #通過ip限制節流
        return self.get_ident(request)

class UserThrottle(SimpleRateThrottle):
    '''登錄用戶60s可以訪問10次'''
    scope = 'userThrottle'    #這裏面的值,自己隨便定義,settings裏面根據這個值配置userThrottle

    def get_cache_key(self, request, view):
        return request.user.user_id

BaseThrottle 代碼如下:

from rest_framework.throttling import BaseThrottle
import time
VISIT_RECORD = {}   #保存訪問記錄

class VisitThrottle(BaseThrottle):
    '''60s內只能訪問3次'''

    def __init__(self):
        self.history = None   #初始化訪問記錄

    def allow_request(self,request,view):
        #獲取用戶ip (get_ident)
        remote_addr = self.get_ident(request)
        ctime = time.time()
        #如果當前IP不在訪問記錄裏面,就添加到記錄
        if remote_addr not in VISIT_RECORD:
            VISIT_RECORD[remote_addr] = [ctime,]     #鍵值對的形式保存
            return True    #True表示可以訪問
        #獲取當前ip的歷史訪問記錄
        history = VISIT_RECORD.get(remote_addr)
        #初始化訪問記錄
        self.history = history

        #如果有歷史訪問記錄,並且最早一次的訪問記錄離當前時間超過60s,就刪除最早的那個訪問記錄,
        #只要為True,就一直循環刪除最早的一次訪問記錄
        while history and history[-1] < ctime - 60:
            history.pop()
        #如果訪問記錄不超過三次,就把當前的訪問記錄插到第一個位置(pop刪除最後一個)
        if len(history) < 3:
            history.insert(0,ctime)
            return True

    def wait(self):
        '''還需要等多久才能訪問'''
        ctime = time.time()
        return 60 - (ctime - self.history[-1])

配置節流

#全局
REST_FRAMEWORK = {
    # 設置全局節流
    "DEFAULT_THROTTLE_CLASSES":['api.utils.throttle.UserThrottle'],   #全局配置,登錄用戶節流限制(10/m)
    # 設置訪問頻率
    "DEFAULT_THROTTLE_RATES":{
        'throttle':'3/m',         #沒登錄用戶3/m,throttle就是scope定義的值,通過IP地址
        'userThrottle':'10/m',    #登錄用戶10/m,userThrottle就是scope定義的值, 通過user_id
    }
}

# 局部:在類視圖中添加
throttle_classes = [VisitThrottle,]

大概就這些吧,官方文檔真的很詳細,看官方文檔可以解決95%的疑問,加上Django和python自帶的快速開發生產力,寫起來太爽啦~

歡迎交流

我整理了一系列的技術文章和資料,在公眾號「程序設計實驗室」後台回復 linux、flutter、c#、netcore、android、java、python 等可獲取相關技術文章和資料