Skip to content

Latest commit

 

History

History
353 lines (271 loc) · 18.5 KB

49.RESTful架构和DRF入门.md

File metadata and controls

353 lines (271 loc) · 18.5 KB

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請求的頭資訊中用AcceptContent-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>{{ subject.name }}學科的老師資訊</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>姓名:{{ teacher.name }}</strong></span>
                    <span>性別:{{ teacher.sex | maleOrFemale }}</span>
                    <span>出生日期:{{ teacher.birth }}</span>
                </div>
                <div class="intro">{{ teacher.intro }}</div>
                <div class="comment">
                    <a href="" @click.prevent="vote(teacher, true)">好評</a>&nbsp;&nbsp;
                    (<strong>{{ teacher.good_count }}</strong>)
                    &nbsp;&nbsp;&nbsp;&nbsp;
                    <a href="" @click.prevent="vote(teacher, false)">差評</a>&nbsp;&nbsp;
                    (<strong>{{ teacher.bad_count }}</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的使用者跟蹤具體流程如下:

  1. 使用者登入時,如果登入成功就按照某種方式為使用者生成一個令牌(token),該令牌中通常包含了使用者標識、過期時間等資訊而且需要加密並生成指紋(避免偽造或篡改令牌),伺服器將令牌返回給前端;
  2. 前端獲取到伺服器返回的token,儲存在瀏覽器本地儲存中(可以儲存在localStoragesessionStorage中,對於使用Vue.js的前端專案來說,還可以透過Vuex進行狀態管理);
  3. 對於使用了前端路由的專案來說,前端每次路由跳轉,可以先判斷localStroage中有無token,如果沒有則跳轉到登入頁;
  4. 每次請求後端資料介面,在HTTP請求頭裡攜帶token;後端介面判斷請求頭有無token,如果沒有token以及token是無效的或過期的,伺服器統一返回401;
  5. 如果前端收到HTTP響應狀態碼401,則重定向到登入頁面。

透過上面的描述,相信大家已經發現了,基於token的使用者跟蹤最為關鍵是在使用者登入成功時,要為使用者生成一個token作為使用者的身份標識。生成token的方法很多,其中一種比較成熟的解決方案是使用JSON Web Token。

JWT概述

JSON Web Token通常簡稱為JWT,它是一種開放標準(RFC 7519)。隨著RESTful架構的流行,越來越多的專案使用JWT作為使用者身份認證的方式。JWT相當於是三個JSON物件經過編碼後,用.分隔並組合到一起,這三個JSON物件分別是頭部(header)、載荷(payload)和簽名(signature),如下圖所示。

  1. 頭部

    {
      "alg": "HS256",
      "typ": "JWT"
    }

    其中,alg屬性表示簽名的演算法,預設是HMAC SHA256(簡寫成HS256);typ屬性表示這個令牌的型別,JWT中都統一書寫為JWT

  2. 載荷

    載荷部分用來存放實際需要傳遞的資料。JWT官方文件中規定了7個可選的欄位:

    • iss :簽發人
    • exp:過期時間
    • sub:主題
    • aud:受眾
    • nbf:生效時間
    • iat:簽發時間
    • jti:編號

    除了官方定義的字典,我們可以根據應用的需要新增自定義的欄位,如下所示。

    {
      "sub": "1234567890",
      "nickname": "jackfrued",
      "role": "admin"
    }
  3. 簽名

    簽名部分是對前面兩部分生成一個指紋,防止資料偽造和篡改。實現簽名首先需要指定一個金鑰。這個金鑰只有伺服器才知道,不能洩露給使用者。然後,使用頭部指定的簽名演算法(預設是HS256),按照下面的公式產生簽名。

    HS256(base64Encode(header) + '.' + base64Encode(payload), secret)

    算出簽名以後,把頭部、載荷、簽名三個部分拼接成一個字串,每個部分用.進行分隔,這樣一個JWT就生成好了。

JWT的優缺點

使用JWT的優點非常明顯,包括:

  1. 更容易實現水平擴充套件,因為令牌儲存在瀏覽器中,伺服器不需要做狀態管理。
  2. 更容易防範CSRF攻擊,因為在請求頭中新增localStoragesessionStorage中的token必須靠JavaScript程式碼完成,而不是自動新增到請求頭中的。
  3. 可以防偽造和篡改,因為JWT有簽名,偽造和篡改的令牌無法透過簽名驗證,會被認定是無效的令牌。

當然,任何技術不可能只有優點沒有缺點,JWT也有諸多缺點,大家需要在使用的時候引起注意,具體包括:

  1. 可能會遭受到XSS攻擊(跨站指令碼攻擊),透過注入惡意指令碼執行JavaScript程式碼獲取到使用者令牌。
  2. 在令牌過期之前,無法作廢已經頒發的令牌,要解決這個問題,還需要額外的中間層和程式碼來輔助。
  3. 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天的內容,裡面提供了完整的投票專案程式碼的地址。