一小時完成後臺開發:DjangoRestFramework開發實踐
阿新 • • 發佈:2020-07-12
# DjangoRestFramework開發實踐
在這之前我寫過一篇關於Django與Drf快速開發實踐的部落格,[Django快速開發實踐:Drf框架和xadmin配置指北](https://zhuanlan.zhihu.com/p/100135134),粗略說了一下Drf配置和基本使用,不過裡面只是涉及到最基本的CRUD,在正常的後端開發中涉及的諸如認證和許可權、訊息佇列、快取之類的操作,上一篇部落格並沒有涉及,這次開發我仔細了看了官方文件的這幾個部分,把這部分的功能完善了起來。
Drf的設計很有Django的味道,(畢竟就是伴生框架嘛)封裝了很多功能,稍微配置一下就可以用,這點在快速開發方面真的好評。
開始進入正題。
## 認證和授權
任何系統都離不開認證和授權,Drf內建一套強大的鑑權系統,框架提供了幾種基本的認證和許可權控制方式,小型系統基本夠用,還可以自定義許可權中介軟體,很方便就能實現節流這樣的功能。
### API認證
Drf內建的四種API認證方式,基本資訊我做了一個表格:
| 認證方式 | 說明 |
| ------------------------------------------------------------ | --------------------------------------------------- |
| [BasicAuthentication](https://www.django-rest-framework.org/api-guide/authentication/#basicauthentication) | 每次提交請求的時候附加使用者名稱和密碼來進行認證 |
| [TokenAuthentication](https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication) | 每次提交請求的時候在HTTP headers裡附加Token進行認證 |
| [SessionAuthentication](https://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication) | 使用者登入之後系統在cookies存入sessionid進行認證 |
| [RemoteUserAuthentication](https://www.django-rest-framework.org/api-guide/authentication/#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`中可以配置預設的認證方式,這裡我添加了三個:
```python
REST_FRAMEWORK = {
# 身份驗證
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}
```
當然也支援各種第三方的認證框架,比如下面這些:
- [Django OAuth Toolkit](https://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit)
- [Django REST framework OAuth](https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth)
- [JSON Web Token Authentication](https://www.django-rest-framework.org/api-guide/authentication/#json-web-token-authentication)
- 還有很多,詳見官方文件~
使用這些認證方式,認證通過後,在views裡面` request.user `就是一個Django使用者物件,如果未認證,就是一個`AnonymousUser`物件。
接下來說說許可權
### API授權
Drf的介面許可權有以下幾種:
- [AllowAny](https://www.django-rest-framework.org/api-guide/permissions/#allowany):允許所有,登不登入無所謂
- [IsAuthenticated](https://www.django-rest-framework.org/api-guide/permissions/#isauthenticated):登入了才能訪問
- [IsAdminUser](https://www.django-rest-framework.org/api-guide/permissions/#isadminuser):管理員才能訪問
- [IsAuthenticatedOrReadOnly](https://www.django-rest-framework.org/api-guide/permissions/#isauthenticatedorreadonly):顧名思義,不登入只讀,登入才能寫入
- [DjangoModelPermissions](https://www.django-rest-framework.org/api-guide/permissions/#djangomodelpermissions):根據Django Auth的配置(許可權細化到每個model)
- [DjangoModelPermissionsOrAnonReadOnly](https://www.django-rest-framework.org/api-guide/permissions/#djangomodelpermissionsoranonreadonly)
- [DjangoObjectPermissions](https://www.django-rest-framework.org/api-guide/permissions/#djangoobjectpermissions):配合第三方許可權控制,細化到每個物件
一般來說小網站用到`DjangoModelPermissions`就是夠用的,或者乾脆簡單一點,用`IsAuthenticated`和`queryset`限定請求的資料即可。
介紹完了基本概念,來看看程式碼中是如何操作的。
### 在viewset中的使用
對於操作使用者資訊的viewset,我只用了`permissions.IsAuthenticated`這個許可權,然後覆蓋了`ReadOnlyModelViewSet`的`get_queryset`方法,把queryset變成只包括當前使用者,這樣就保證了一個使用者只能操作自己的資訊。
```python
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同樣可以使用許可權,加在裝飾器的引數上即可:
```python
@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上應該可以很好理解。
- 對列表:`http://hostname/viewset/some_action/`
- 對單個物件:`http://hostname/viewset/1/some_action/`
> **這部分的參考資料**(優先閱讀官方文件):
>
> - [ 官網文件 ](https://www.django-rest-framework.org/)
>
> - [Python系列之《Django-DRF-許可權》]( [https://nicksors.cc/2018/07/25/Python%E7%B3%BB%E5%88%97%E4%B9%8B%E3%80%8ADjango-DRF-%E6%9D%83%E9%99%90%E3%80%8B.html](https://nicksors.cc/2018/07/25/Python系列之《Django-DRF-許可權》.html) )
> - [Django-drf架構 認證、許可權、節流的詳解]( https://blog.csdn.net/Fe_cow/article/details/91489476 )
## ApiView和ViewSet
Drf的ApiView相當於Django的View,每個ViewSet是一組Restful的介面,看看viewset的程式碼,其中定義了6個action,對應get、post、put、delete這些http方法。
```python
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,通過重寫`get`、`post`這些方法可以實現不同的http方法響應。
ApiView和ViewSet同樣通過在類欄位中加入`authentication_classes`和`permission_classes`實現認證和授權。
## 分頁 PAGINATION
Drf和Django一樣自帶分頁功能,很好用(當然也支援使用第三方的分頁功能)。
首先進行配置(不配置的話使用預設配置),這裡我設定每頁顯示十條記錄:
```python
REST_FRAMEWORK = {
# 設定分頁
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
```
使用得最多的`ModelViewSet`已經自帶分頁了,這個我們不用操心,不過如果自己定義了action來返回列表資料的話,就沒有分頁,這時候要用`paginate_queryset`方法來處理。
程式碼如下:
```python
@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文件,首先配置:
```python
REST_FRAMEWORK = {
# 文件
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
```
在使用之前還需要使用pip安裝coreapi:
```bash
pip install coreapi
```
所有的ViewSet只要用了Router註冊的話都預設新增到文件裡,不過ApiView的話是要自己新增的。
```python
from rest_framework.schemas.coreapi import AutoSchema
class SomeView(APIView):
schema = AutoSchema()
def post(self, request):
data = dosomething()
return Response(data)
```
這樣就可以了,別忘了註冊路由,路由我在下文統一介紹。
配置完成之後文件的效果應該是下圖這樣,這個頁面可以測試請求,還可以以各種方式登入認證,比Drf預設的介面豐富一些~
![](https://upload-images.jianshu.io/upload_images/8869373-a67cce9d88ffa22e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
## 路由
使用Drf框架可以有兩種路由方式,一種是Drf的路由,一直是Django的路由,兩種搭配使用。
### Drf的Router
```python
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`裡面。
```python
urlpatterns = [
path('api/', include(router.urls)),
]
```
對於ApiView,路由和Django的ClassBaseView差不多:
```python
urlpatterns = [
path('login/', views.LoginView.as_view()),
path('signup/', views.SignUpView.as_view()),
]
```
### 配置認證和文件相關url
```python
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之類的結構化資料。
使用起來很簡單,我一般都是這麼定義
```python
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = '__all__'
```
使用`__all__`直接把所有模型欄位都包括進去,針對外來鍵等關係欄位,我們需要做一些其他處理。
這裡簡單介紹一下我常用的幾種關係欄位處理方式
- `StringRelatedField`:將關係欄位顯示為一個字串,這個字串取決於該模型定義的`__str__`方法
- `PrimaryKeyRelatedField`:顯示成該關係欄位的主鍵,這個也是預設的
- 巢狀序列化:定義一個該欄位對應型別的序列化器,巢狀序列化
具體的使用方式可以在官網找到~
下面再介紹幾個常用的引數:
- `read_only`:一般針對不想被修改的欄位可以使用,比如說使用者id
- `many`:用於多對多關係或者一對多,會將所有引用序列化為一個列表
## 限流
其實就是一個自定義的認證過程。
Drf內建有` BaseThrottle `、` SimpleRateThrottle`,後者是前者的之類。
- ` BaseThrottle ` 需要自己寫`allow_request`和`wait`方法,控制粒度更細
- ` SimpleRateThrottle`重寫` get_cache_key `和設定` scope `名稱就可以,更簡單
### 實現1分鐘內只能訪問3次的限流
` SimpleRateThrottle`程式碼如下:
```python
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 ` 程式碼如下:
```python
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])
```
### 配置節流
```python
#全域性
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 等可獲取相關技術文章