Python 100 Days Day49 RESTful and DRF
21 Sep 2022 | beginner pythonauthor: jackfrued
RESTful架構和DRF入門
把軟件(Software)、平台(Platform)、基礎設施(Infrastructure)做成服務(Service)是很多IT企業都一直在做的事情,這就是大家經常聽到的SasS(軟件即服務)、PasS(平台即服務)和IasS(基礎設置即服務)。實現面向服務的架構(SOA)有諸多的方式,包括RPC(遠程過程調用)、Web Service、REST等,在技術層面上,SOA是一種抽象的、松散耦合的粗粒度軟件架構;在業務層面上,SOA的核心概念是“重用”和“互操作”,它將系統資源整合成可操作的、標準的服務,使得這些資源能夠被重新組合和應用。在實現SOA的諸多方案中,REST被認為是最適合互聯網應用的架構,符合REST規範的架構也經常被稱作RESTful架構。
REST概述
REST這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的,Roy是HTTP協議(1.0和1.1版)的主要設計者、Apache服務器軟件主要作者、Apache基金會第一任主席。在他的博士論文中,Roy把他對互聯網軟件的架構原則定名為REST,即REpresentational State Transfer的縮寫,中文通常翻譯為“表現層狀態轉移”或“表述狀態轉移”。
這里的“表現層”其實指的是“資源”的“表現層”。所謂資源,就是網絡上的一個實體,或者說是網絡上的一個具體信息。它可以是一段文本、一張圖片、一首歌曲或一種服務。我們可以用一個URI(統一資源定位符)指向資源,要獲取到這個資源,訪問它的URI即可,URI就是資源在互聯網上的唯一標識。資源可以有多種外在表現形式。我們把資源具體呈現出來的形式,叫做它的“表現層”。比如,文本可以用text/plain格式表現,也可以用text/html格式、text/xml格式、application/json格式表現,甚至可以采用二進制格式;圖片可以用image/jpeg格式表現,也可以用image/png格式表現。URI只代表資源的實體,不代表它的表現形式。嚴格地說,有些網址最後的.html後綴名是不必要的,因為這個後綴名表示格式,屬於“表現層”範疇,而URI應該只代表“資源”的位置,它的具體表現形式,應該在HTTP請求的頭信息中用Accept和Content-Type字段指定,這兩個字段才是對“表現層”的描述。
訪問一個網站,就代表了客戶端和服務器的一個互動過程。在這個過程中,勢必涉及到數據和狀態的變化。Web應用通常使用HTTP作為其通信協議,客戶端想要操作服務器,必須通過HTTP請求,讓服務器端發生“狀態轉移”,而這種轉移是建立在表現層之上的,所以就是“表現層狀態轉移”。客戶端通過HTTP的動詞GET、POST、PUT(或PATCH)、DELETE,分別對應對資源的四種基本操作,其中GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT(或PATCH)用來更新資源,DELETE用來刪除資源。
簡單的說RESTful架構就是:“每一個URI代表一種資源,客戶端通過四個HTTP動詞,對服務器端資源進行操作,實現資源的表現層狀態轉移”。
我們在設計Web應用時,如果需要向客戶端提供資源,就可以使用REST風格的URI,這是實現RESTful架構的第一步。當然,真正的RESTful架構並不只是URI符合REST風格,更為重要的是“無狀態”和“冪等性”兩個詞,我們在後面的課程中會為大家闡述這兩點。下面的例子給出了一些符合REST風格的URI,供大家在設計URI時參考。
| 請求方法(HTTP動詞) | URI | 解釋 |
|---|---|---|
| GET | /students/ |
獲取所有學生 |
| POST | /students/ |
新建一個學生 |
| GET | /students/ID/ |
獲取指定ID的學生信息 |
| PUT | /students/ID/ |
更新指定ID的學生信息(提供該學生的全部信息) |
| PATCH | /students/ID/ |
更新指定ID的學生信息(提供該學生的部分信息) |
| DELETE | /students/ID/ |
刪除指定ID的學生信息 |
| GET | /students/ID/friends/ |
列出指定ID的學生的所有朋友 |
| DELETE | /students/ID/friends/ID/ |
刪除指定ID的學生的指定ID的朋友 |
DRF使用入門
在Django項目中,如果要實現REST架構,即將網站的資源發布成REST風格的API接口,可以使用著名的三方庫djangorestframework ,我們通常將其簡稱為DRF。
安裝和配置DRF
安裝DRF。
pip install djangorestframework
配置DRF。
INSTALLED_APPS = [
'rest_framework',
]
# 下面的配置根據項目需要進行設置
REST_FRAMEWORK = {
# 配置默認頁面大小
# 'PAGE_SIZE': 10,
# 配置默認的分頁類
# 'DEFAULT_PAGINATION_CLASS': '...',
# 配置異常處理器
# 'EXCEPTION_HANDLER': '...',
# 配置默認解析器
# 'DEFAULT_PARSER_CLASSES': (
# 'rest_framework.parsers.JSONParser',
# 'rest_framework.parsers.FormParser',
# 'rest_framework.parsers.MultiPartParser',
# ),
# 配置默認限流類
# 'DEFAULT_THROTTLE_CLASSES': (
# '...'
# ),
# 配置默認授權類
# 'DEFAULT_PERMISSION_CLASSES': (
# '...',
# ),
# 配置默認認證類
# 'DEFAULT_AUTHENTICATION_CLASSES': (
# '...',
# ),
}
編寫序列化器
前後端分離的開發需要後端為前端、移動端提供API數據接口,而API接口通常情況下都是返回JSON格式的數據,這就需要對模型對象進行序列化處理。DRF中封裝了Serializer類和ModelSerializer類用於實現序列化操作,通過繼承Serializer類或ModelSerializer類,我們可以自定義序列化器,用於將對象處理成字典,代碼如下所示。
from rest_framework import serializers
class SubjectSerializer(serializers.ModelSerializer):
class Meta:
model = Subject
fields = '__all__'
上面的代碼直接繼承了ModelSerializer,通過Meta類的model屬性指定要序列化的模型以及fields屬性指定需要序列化的模型字段,稍後我們就可以在視圖函數中使用該類來實現對Subject模型的序列化。
編寫視圖函數
DRF框架支持兩種實現數據接口的方式,一種是FBV(基於函數的視圖),另一種是CBV(基於類的視圖)。我們先看看FBV的方式如何實現數據接口,代碼如下所示。
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(('GET', ))
def show_subjects(request: HttpRequest) -> HttpResponse:
subjects = Subject.objects.all().order_by('no')
# 創建序列化器對象並指定要序列化的模型
serializer = SubjectSerializer(subjects, many=True)
# 通過序列化器的data屬性獲得模型對應的字典並通過創建Response對象返回JSON格式的數據
return Response(serializer.data)
對比上一個章節的使用bpmapper實現模型序列化的代碼,使用DRF的代碼更加簡單明了,而且DRF本身自帶了一套頁面,可以方便我們查看我們使用DRF定制的數據接口,如下圖所示。

直接使用上一節寫好的頁面,就可以通過Vue.js把上面接口提供的學科數據渲染並展示出來,此處不再進行贅述。
實現老師信息數據接口
編寫序列化器。
class SubjectSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Subject
fields = ('no', 'name')
class TeacherSerializer(serializers.ModelSerializer):
class Meta:
model = Teacher
exclude = ('subject', )
編寫視圖函數。
@api_view(('GET', ))
def show_teachers(request: HttpRequest) -> HttpResponse:
try:
sno = int(request.GET.get('sno'))
subject = Subject.objects.only('name').get(no=sno)
teachers = Teacher.objects.filter(subject=subject).defer('subject').order_by('no')
subject_seri = SubjectSimpleSerializer(subject)
teacher_seri = TeacherSerializer(teachers, many=True)
return Response({'subject': subject_seri.data, 'teachers': teacher_seri.data})
except (TypeError, ValueError, Subject.DoesNotExist):
return Response(status=404)
配置URL映射。
urlpatterns = [
path('api/teachers/', show_teachers),
]
通過Vue.js渲染頁面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>老師信息</title>
<style>
/* 此處省略掉層疊樣式表 */
</style>
</head>
<body>
<div id="container">
<h1>學科的老師信息</h1>
<hr>
<h2 v-if="loaded && teachers.length == 0">暫無該學科老師信息</h2>
<div class="teacher" v-for="teacher in teachers">
<div class="photo">
<img :src="'/static/images/' + teacher.photo" height="140" alt="">
</div>
<div class="info">
<div>
<span><strong>姓名:</strong></span>
<span>性別:</span>
<span>出生日期:</span>
</div>
<div class="intro"></div>
<div class="comment">
<a href="" @click.prevent="vote(teacher, true)">好評</a>
(<strong></strong>)
<a href="" @click.prevent="vote(teacher, false)">差評</a>
(<strong></strong>)
</div>
</div>
</div>
<a href="/static/html/subjects.html">返回首頁</a>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js"></script>
<script>
let app = new Vue({
el: '#container',
data: {
subject: {},
teachers: [],
loaded: false
},
created() {
fetch('/api/teachers/' + location.search)
.then(resp => resp.json())
.then(json => {
this.subject = json.subject
this.teachers = json.teachers
})
},
filters: {
maleOrFemale(sex) {
return sex? '男': '女'
}
},
methods: {
vote(teacher, flag) {
let url = flag? '/praise/' : '/criticize/'
url += '?tno=' + teacher.no
fetch(url).then(resp => resp.json()).then(json => {
if (json.code === 10000) {
if (flag) {
teacher.good_count = json.count
} else {
teacher.bad_count = json.count
}
}
})
}
}
})
</script>
</body>
</html>
前後端分離下的用戶登錄
之前我們提到過, HTTP是無狀態的,一次請求結束連接斷開,下次服務器再收到請求,它就不知道這個請求是哪個用戶發過來的。但是對於一個Web應用而言,它是需要有狀態管理的,這樣才能讓服務器知道HTTP請求來自哪個用戶,從而判斷是否允許該用戶請求以及為用戶提供更好的服務,這個過程就是常說的會話管理。
之前我們做會話管理(用戶跟蹤)的方法是:用戶登錄成功後,在服務器端通過一個session對象保存用戶相關數據,然後把session對象的ID寫入瀏覽器的cookie中;下一次請求時,HTTP請求頭中攜帶cookie的數據,服務器從HTTP請求頭讀取cookie中的sessionid,根據這個標識符找到對應的session對象,這樣就能夠獲取到之前保存在session中的用戶數據。我們剛才說過,REST架構是最適合互聯網應用的架構,它強調了HTTP的無狀態性,這樣才能保證應用的水平擴展能力(當並發訪問量增加時,可以通過增加新的服務器節點來為系統擴容)。顯然,基於session實現用戶跟蹤的方式需要服務器保存session對象,在做水平擴展增加新的服務器節點時,需要覆制和同步session對象,這顯然是非常麻煩的。解決這個問題有兩種方案,一種是架設緩存服務器(如Redis),讓多個服務器節點共享緩存服務並將session對象直接置於緩存服務器中;另一種方式放棄基於session的用戶跟蹤,使用基於token的用戶跟蹤。
基於token的用戶跟蹤是在用戶登錄成功後,為用戶生成身份標識並保存在瀏覽器本地存儲(localStorage、sessionStorage、cookie等)中,這樣的話服務器不需要保存用戶狀態,從而可以很容易的做到水平擴展。基於token的用戶跟蹤具體流程如下:
- 用戶登錄時,如果登錄成功就按照某種方式為用戶生成一個令牌(token),該令牌中通常包含了用戶標識、過期時間等信息而且需要加密並生成指紋(避免偽造或篡改令牌),服務器將令牌返回給前端;
- 前端獲取到服務器返回的token,保存在瀏覽器本地存儲中(可以保存在
localStorage或sessionStorage中,對於使用Vue.js的前端項目來說,還可以通過Vuex進行狀態管理); - 對於使用了前端路由的項目來說,前端每次路由跳轉,可以先判斷
localStroage中有無token,如果沒有則跳轉到登錄頁; - 每次請求後端數據接口,在HTTP請求頭里攜帶token;後端接口判斷請求頭有無token,如果沒有token以及token是無效的或過期的,服務器統一返回401;
- 如果前端收到HTTP響應狀態碼401,則重定向到登錄頁面。
通過上面的描述,相信大家已經發現了,基於token的用戶跟蹤最為關鍵是在用戶登錄成功時,要為用戶生成一個token作為用戶的身份標識。生成token的方法很多,其中一種比較成熟的解決方案是使用JSON Web Token。
JWT概述
JSON Web Token通常簡稱為JWT,它是一種開放標準(RFC 7519)。隨著RESTful架構的流行,越來越多的項目使用JWT作為用戶身份認證的方式。JWT相當於是三個JSON對象經過編碼後,用.分隔並組合到一起,這三個JSON對象分別是頭部(header)、載荷(payload)和簽名(signature),如下圖所示。

-
頭部
{ "alg": "HS256", "typ": "JWT" }其中,
alg屬性表示簽名的算法,默認是HMAC SHA256(簡寫成HS256);typ屬性表示這個令牌的類型,JWT中都統一書寫為JWT。 -
載荷
載荷部分用來存放實際需要傳遞的數據。JWT官方文檔中規定了7個可選的字段:
- iss :簽發人
- exp:過期時間
- sub:主題
- aud:受眾
- nbf:生效時間
- iat:簽發時間
- jti:編號
除了官方定義的字典,我們可以根據應用的需要添加自定義的字段,如下所示。
{ "sub": "1234567890", "nickname": "jackfrued", "role": "admin" } -
簽名
簽名部分是對前面兩部分生成一個指紋,防止數據偽造和篡改。實現簽名首先需要指定一個密鑰。這個密鑰只有服務器才知道,不能泄露給用戶。然後,使用頭部指定的簽名算法(默認是
HS256),按照下面的公式產生簽名。HS256(base64Encode(header) + '.' + base64Encode(payload), secret)算出簽名以後,把頭部、載荷、簽名三個部分拼接成一個字符串,每個部分用
.進行分隔,這樣一個JWT就生成好了。
JWT的優缺點
使用JWT的優點非常明顯,包括:
- 更容易實現水平擴展,因為令牌保存在瀏覽器中,服務器不需要做狀態管理。
- 更容易防範CSRF攻擊,因為在請求頭中添加
localStorage或sessionStorage中的token必須靠JavaScript代碼完成,而不是自動添加到請求頭中的。 - 可以防偽造和篡改,因為JWT有簽名,偽造和篡改的令牌無法通過簽名驗證,會被認定是無效的令牌。
當然,任何技術不可能只有優點沒有缺點,JWT也有諸多缺點,大家需要在使用的時候引起注意,具體包括:
- 可能會遭受到XSS攻擊(跨站腳本攻擊),通過注入惡意腳本執行JavaScript代碼獲取到用戶令牌。
- 在令牌過期之前,無法作廢已經頒發的令牌,要解決這個問題,還需要額外的中間層和代碼來輔助。
- JWT是用戶的身份令牌,一旦泄露,任何人都可以獲得該用戶的所有權限。為了降低令牌被盜用後產生的風險,JWT的有效期應該設置得比較短。對於一些比較重要的權限,使用時應通過其他方式再次對用戶進行認證,例如短信驗證碼等。
使用PyJWT
在Python代碼中,可以使用三方庫PyJWT生成和驗證JWT,下面是安裝PyJWT的命令。
pip install pyjwt
生成令牌。
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1),
'userid': 10001
}
token = jwt.encode(payload, settings.SECRET_KEY).decode()
驗證令牌。
try:
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTQ4NzIzOTEsInVzZXJpZCI6MTAwMDF9.FM-bNxemWLqQQBIsRVvc4gq71y42I9m2zt5nlFxNHUo'
payload = jwt.decode(token, settings.SECRET_KEY)
except InvalidTokenError:
raise AuthenticationFailed('無效的令牌或令牌已經過期')
如果不清楚JWT具體的使用方式,可以先看看第55天的內容,里面提供了完整的投票項目代碼的地址。
Comments