天天看點

RESTful API 設計規範

目錄

REST

RESTful API

RESTful API 設計規範

URI

Request

Response

登入認證問題

動作類型的資源定義

HATEOAS

早古時期,軟體和網絡是兩個不同的領域,前者圍繞着單機環境展開,而後者則研究系統之間的通信。随着網際網路的興起,使得這兩個領域開始融合,首當其沖的就是基于 HTTP 協定的 Web 服務,越來越多的人開始意識到,“網站” 即是 “軟體”。

其中的先驅者就是 Tim Berners-Lee(網際網路的發明者,網際網路聯盟負責人)和 Roy Thomas Fielding(1996 HTTP/1.0、1999 HTTP/1.1 的主要設計者之一,Apache 基金會的第一任主席,Apache Web Server 和 HTTP 協定是共生共榮的關系)等人。

1989年,Tim Berners-Lee 在論文中提出可以在網際網路上建構超連結文檔,并提出了三點基本要素:

URI(Uniform Resource Identifier):統一資源辨別符,是資源(Resource)在網際網路中的唯一辨別。

HTML(Hyper Text Markup Language):超文本标記語言,超文本文檔是由 HTML 标簽組成的描述性文本,HTML 标簽将文字,圖形、動畫、聲音、表格、連結等内容格式進行了統一。

HTTP(Hyper Text Transfer Protocol):超文本傳輸協定,傳輸超文本文檔的傳輸協定,傳輸的資料主體稱為 Message(消息)。

首先我們需要對 Resource 的概念有一個清晰的了解。所謂 Resource,就是網際網路上的一個實體,或者叫具體資訊。它可以是一段文本、一張圖檔、一首歌曲、一種服務,總之就是一個具體實物的抽象。在網際網路中使用 URI 來唯一标記一個 Resource,包含了 URL 和 URN。

URL(Uniform Resource Loader):統一資源定位符,側重于 “定位” 二字。

URN(Uniform Resource Name):統一資源名稱,側重于 “命名” 二字。

舉個例子:尋找一個具體的人(URI)。使用位址來尋找就是 URL:XX省XX市XX區XX單元XX室的主人;使用身份證号和名字來尋找就是 URN:身份證号 + 名字(但無法确認人的位址)。兩者各有場景,但需要注意的是 URL、URN 都是 URI 的子集,但日常生活中最常見的是 URL,是以大家口頭上也習慣使用 URL 來說明一個 Resource。但在實際編碼中,開發者仍要注意 URI 和 URL 的本質差別,注意規範選詞。

RESTful API 設計規範

REST(Representational State Transfer,表現層狀态轉移)最初被 Roy Thomas Fielding 在 2000 年的博士論文《Architectural Styles and the Design of Network-based Software Architectures(架構風格和基于網絡的軟體架構設計)》中提出。顧名思義,Roy Thomas Fielding 在這篇論文中主要讨論的是:如何在符合架構原理的前提下,了解和評估以網絡為基礎的應用軟體的架構設計,得到一個功能強、性能好、适宜通信的架構。

可見,REST、HTTP 協定、Web 網站,三者之間天生聯系緊密。基于此,我們要清晰了解 Representational State Transfer 含義的前提是要對 HTTP 協定有一定的認識。

Representational State Transfer 的含義:

Representational(表現層):即 Resources 的表現層。上述我們知道 Resource 是一種抽象,它具有多種表現形式,而最終把 Resource 具體呈現出來的形式,就叫做它的 “表現層”。例如:一個文本資源可以使用 txt 格式表現,也可以使用 HTML、XML、JSON 等格式表現。在 HTTP/1.0 中,Roy Thomas Fielding 為 HTTP Header 設計了 Accept/Content-Type 字段來描述這個 Resource 的 Representational。

是以,需要注意的是:URI 僅用于表示一個 Resource,不應該在 URI 中描述表現層的内容,一個優雅的 RESTful API 應該使用 Accept/Content-Type 字段來描述 Resource 的表現層。

State Transfer(狀态轉移):Resource 作為 C/S 互動的實體,必然存在着狀态的變化,然而 HTTP 是一種無狀态的協定(不傳輸資源狀态的描述)。這意味着:Resource 的狀态都儲存在服務端,用戶端想要操作某個 Resource(改變其狀态)就必須通過某種手段讓 Resource 在服務端發生狀态的 “轉移”,而且這種 “轉移” 必然是建立在 Resource 的 “表現層” 之上的,例如:建立一個圖檔資源、删除一個圖檔資源;啟動一個服務,關閉一個服務。故稱之為 “表現層狀态轉移”。

而用戶端使用的手段就是 Roy Thomas Fielding 在 HTTP 請求行中設計的 Request Methods(在 HTTP/0.9 中首次引入 GET 方法,在 HTTP/1.0 中首次引入了 POST、Head 方法,在 HTTP/1.1 中引入了 PUT 方法)。

至此,我們回頭再看,REST 讨論的其實是一個:如何将軟體和網絡兩個領域進行交叉融合,繼而得到一個功能強、性能好、适宜通信的網際網路軟體架構的問題。

雖然 REST 架構在今天(2020)的軟體設計中随處可見,但了解其誕生的曆史背景和發展曆程,還是會對這些先驅者們産生由衷的敬意。

*-ful 在西文語境中常用于表示一種風格,RESTful API 就是符合 REST 架構設計思想的軟體 API 風格。

筆者最早認識到 RESTful API,源于時任 AWS CTO 的一封内部郵件,那時是 2006 年,正值 AWS 孵化的初期,印象非常深刻的有兩點:

強調 AWS 必須是一種 Resilient software architecture(具有韌性/彈性的軟體架構)。

不使用 RESTful API 的員工将被辭退。

直到 2013 年筆者接觸 OpenStack(OpenStack 初期常被認為是 AWS 的開源對标版本)之後才更深刻的體會到了其内涵和精髓。一言以蔽之就是:大型分布式軟體的各個元件之間必須具備解耦和擴充的能力,網絡(Network-base)是元件之間通信的唯一依賴,且對資源的操作具有唯一的确定性。

退一步的,我們可以選取一個角度來比較一下 RESTful 與另外兩種常見的分布式架構風格的差別:

RESTful 抽象的是資源,資源的抽象不需要依賴開發平台或程式設計語言,用戶端和服務端完全松耦合。

DO(Distributed Objects,分布式對象)抽象的是對象,而不同的程式設計語言對對象的定義有很大差别,是以 DO 架構通常會與某種程式設計語言(.NET)綁定,若跨語言互動,實作則會非常複雜。架構執行個體有 RMI、EJB/DCOM、.NET Remoting 等。

RPC(Remote Procedure Call,遠端過程調用)抽象的是過程,這要求用戶端和服務端具有很強的耦合度,否者雙方無法了解對方的意圖。是以 RESTful 隻需要以名詞為核心的,而 RPC 則需要以動詞為核心。架構執行個體有 SOAP、XML-RPC、Flash AMF 等。

AWS 奉行的這一鐵律,使其得以在幾年間飛速擴充至上百個核心元件,成為了極具韌性和良好生态的公有雲架構。現在的軟體公司基本都會采用 RESTful API 風格,讓産品可以通過 API 的方式融入到行業生态中,形成 APIs 經濟效益。

核心原則隻有一句話:總是圍繞着 Resource 進行模組化,HTTP URI 辨別資源,HTTP Request Methods 操作資源。

資源命名使用全小寫字母。

資源命名盡量使用複數名詞,名詞間使用 “-” 分隔。

資源命名盡量與關系型資料庫表結構命名一緻。

URI 突出版本号。

URI 不具有表現層描述。

URL 使用 “/” 劃分層級,同時盡量避免使用多級 URI。

使用 HTTP Request Methods 時,要注意方法的 “安全性” 和 “幂等性”:

安全性:指調用 HTTP Request Method,是否會導緻資源狀态變化。

幂等性:指多次調用 HTTP Request Method,隻要輸入不變,那麼執行的結果也是不變的。

Method

功能

描述

安全性

幂等

GET

SELECT

擷取一個(提供資源 ID)或多個(提供 Filter 條件)或全部資源,擷取多個時使用 Query Parameters 進行過濾查詢,使用 Pagination Parameters(e.g. limit、offset、page、sortby)進行分頁查詢。

POST

CREATE

建立一個資源(杜絕一次建立多個資源)。

X

PUT

UPDATE

更新一個資源(提供完整的資源資料)。

PATCH

更新一個資源(提供部分的資源資料)。

DELETE

删除一個資源。

HEAD

擷取資源的中繼資料而非資源本身。此方法經常被用來測試超文本連結的有效性,可通路性,和最近的改變,例如:擷取虛拟機鏡像的屬性資訊。

OPTIONS

擷取資訊,關于資源的哪些屬性是由用戶端決定的。在跨域或使用代理請求時,通常會用到,OPTION 請求在于判定資源的選項或需求,或者伺服器的能力。

傳回一個或多個資源的完整資料和 Status Code,傳回一個時使用 {},傳回多個時使用 [];若錯誤需要傳回錯誤原因和正确提示。

傳回一個資源的完整資料和 Status Code;若錯誤需要傳回錯誤原因和正确提示。

僅傳回 Status Code;若錯誤需要傳回錯誤原因和正确提示。

HTTP Response Status Codes(狀态碼)就是一個三位數,分成五個類别:

1xx:相關資訊(不常用)

RESTful API 設計規範

2xx:操作成功

RESTful API 設計規範

3xx:重定向(不常用)

RESTful API 設計規範

4xx:用戶端錯誤

RESTful API 設計規範

5xx:伺服器錯誤

RESTful API 設計規範

登入認證是 RESTful API 設計中的一個特殊課題,登陸驗證源于使用者使用 Web 應用時記錄使用者身份狀态的需求,其特點是:

用戶端和服務端都記錄了使用者的賬戶資訊,用戶端請求時攜帶賬戶資訊到服務端進行身份認證。

認證成功後,一次登入持久線上,或者設定登陸狀态的過期時間。

RESTful API 設計規範

在 RESTful 設計中通常使用 Cookies 或 Token 的方式來實作登陸驗證的需求:

Cookie 方式:因為 HTTP 協定是無狀态的,是以一般使用 Cookie 來解決會話狀态的儲存,以彌補無法進行會話跟蹤的不足。

在登陸時,在服務端進行身份認證,通過則建立 Session,記錄使用者登陸狀态及相關資訊,并将 Session ID 傳回;

用戶端接收響應後,将 Session ID 存放在 Cookie 中(浏覽器可自動記錄并存儲);

用戶端的後續請求,直接在 Request Header 中攜帶對應 Cookie 中存放的 Session ID;

服務端接收請求後,驗證 Session ID 是否合法,若合法則進行相關處理并給出響應。

Token 方式:

在登陸時,在服務端進行身份認證,通過則傳回 Token,Token 具有過期時間。

用戶端的後續請求,都攜帶者 Token 進行請求。傳回client後,client需要通過腳本控制存放token資訊;

顯然,Token 是更加推薦的方式,因為 Cookie 方式會在 HTTP Header 中保持一個狀态(Session ID)。這個狀态會要求該請求隻能被存儲了對應的 Session 進行處理,這一點違背了 REST 架構的原則。

現實情況中,總有一些場景(資源)是 HTTP Request Methods 所抽象不了的。上述的登陸驗證是一個典型的例子。針對登陸驗證場景,可以把使用者在遠端伺服器的會話資訊抽象為一個資源,這樣的話,登陸動作其實就是在遠端伺服器增加了一個會話資源,反正,登出就是删除一個會話資源,是以 RESTful API 可以這樣設計:

再比如,網上彙款場景,将彙款的動作定義為一種服務:

再比如,OpenStack 的虛拟機操作 start、stop、reboot、migration 等,将這些操作定義為一個 os-action 資源,然後通過不同在 Request Body 中使用不同的内容來進行區分:

簡而言之,如果某些動作是 HTTP Method 動詞所表示不了的,就應該把這個動作做成一種資源。

Roy Thomas Fielding 在論文中還提到了 HATEOAS(Hypermedia as the Engine of Application State,超媒體作為應用狀态的引擎)的概念。

所謂 “應用狀态”,即:用戶端的狀态,可以了解為會話狀态。服務端以 HyperMedia(超媒體的)形式将資源展示在用戶端,當用戶端通路其中的超媒體的連結(URI)時,就可以擷取該連結關聯的資源或者可以對資源執行特定的操作。擷取的資源或者經特定操作響應後的資源在經過同樣 Resource Request Handler 确定表現層後,繼續以超媒體的形式在表現在用戶端中。而這種資源内容或形式的改變都會導緻用戶端會話狀态的改變,是以媒體就成為了驅動用戶端會話狀态轉換的引擎(應用狀态的改變基于超媒體的改變)。

簡而言之,HATEOAS 就是不斷的在 Response Body 加入 HyperMedia API 連結,以供用戶端進行調用。

通常的,API 的調用者完全掌握了 URI 是怎麼設計的。一個解決方法就是在 Response Body 中給出相關連結,便于用戶端進行下一步操作的判斷。使得使用者需要查閱繁瑣的文檔,也知道下一步應該怎麼做。

Github 的 API 就實作了 HATEOAS,通路 api.github.com 會得到一個所有可用 API 的網址清單:

從上面可以看到,如果想擷取目前使用者的資訊,應該繼續通路 api.github.com/user,然後就得到了下面結果:

伺服器的傳回中提示了相關文檔的網址。

HATEOAS 格式并沒有統一的标準,上面例子中,GitHub 就将它們與其他屬性放在了一起。其實,更好的做法應該是将相關連結與其他屬性分開在不同的區間中:

OpenStack 也大量的使用到了這種設計: