REST over HTTP

RESTRoy Fielding 在他的論文 (第 5 章是 REST 的表示)中提出的與協議無關的體系結構,它將 Web 瀏覽器的經過驗證的概念概括為客戶端,以便將分散式系統中的客戶端與伺服器分離。

為了使服務或 API 成為 RESTful,它必須遵守給定的約束,例如:

  • 客戶端伺服器
  • 無狀態
  • 可快取
  • 分層系統
  • 統一介面
    • 資源識別
    • 資源代表
    • 自我描述性的資訊
    • 超媒體

除了 Fielding 的論文中提到的限制之外,在他的部落格文章中, REST API 必須是超文字驅動的 ,Fielding 澄清說只是通過 HTTP 呼叫服務並不能使它成為 RESTful 。因此,服務還應遵守進一步的規則,概述如下:

  • API 應遵守並且不違反基礎協議。儘管大多數時候都是通過 HTTP 使用 REST,但它並不侷限於此協議。

  • 通過媒體型別強烈關注資源及其呈現。

  • 客戶不應該對 API 中的可用資源或其返回狀態( 型別化資源 ) 有初步瞭解或假設,而是通過已發出的請求和分析的響應動態學習它們。這使伺服器有機會在不破壞客戶端實現的情況下輕鬆移動或重新命名資源。

理查森成熟度模型

理查德森成熟度模型是為了獲得 RESTful Web 服務通過 HTTP REST 應用約束的方式。

Leonard Richardson 將應用程式分為以下 4 個層次:

  • 0 級:使用 HTTP 進行傳輸
  • 第 1 級:使用 URL 來識別資源
  • 第 2 級:使用 HTTP 動詞和狀態進行互動
  • 3 級:使用 HATEOAS

由於重點是資源狀態的表示,因此鼓勵支援同一資源的多個表示。因此,表示可以呈現資源狀態的概述,而另一個表示返回相同資源的完整細節。

另請注意,給定 Fielding 約束,只有在實現 RMM 的第 3 級時,API 才有效地進行 RESTful

HTTP 請求和響應

HTTP 請求是:

  • 一個動詞(又名方法),大多數時候是 GETPOSTPUTDELETEPATCH 之一
  • 一個 URL
  • 標題(鍵值對)
  • 可選的身體(又名有效載荷,資料)

HTTP 響應是:

HTTP 動詞特徵:

  • 有身體的動詞:POSTPUTPATCH
  • 必須安全的動詞(即不得修改資源):GET
  • 動詞必須是冪等的(即多次執行時不得再次影響資源):GET(nullipotent),PUTDELETE
        body  safe  idempotent
GET      ✗     ✔     ✔
POST     ✔     ✗     ✗
PUT      ✔     ✗     ✔
DELETE   ✗     ✗     ✔
PATCH    ✔     ✗     ✗

因此,可以將 HTTP 謂詞與 CRUD 函式進行比較 :

請注意, PUT 請求要求客戶端使用更新的值傳送整個資源。要部分更新資源,可以使用 PATCH 動詞(請參閱如何部分更新資源? )。

通常的 HTTP 響應狀態

成功

重定向

客戶錯誤

伺服器錯誤

筆記

沒有什麼可以阻止你為錯誤的反應新增正文,以使客戶拒絕更清楚。例如, 422(UNPROCESSABLE ENTITY) 有點模糊:響應主體應該提供無法處理實體的原因。

HATEOAS

每個資源必須為其連結的資源提供超媒體。連結至少由以下內容組成:

  • rel(for rel ation,aka name):描述主要資源和連結資源之間的關係
  • 一個 href:定位連結資源的 URL

還可以使用其他屬性來幫助棄用,內容協商等。

Cormac Mulhall 解釋說*,客戶端應根據嘗試的內容決定使用什麼 HTTP 動詞*。如有疑問,API 文件無論如何都應該幫助你瞭解與所有超媒體的可用互動。

媒體型別

媒體型別有助於提供自我描述性訊息。它們扮演客戶端和伺服器之間的合同的一部分,以便他們可以交換資源和超媒體。

儘管 application/jsonapplication/xml 是非常流行的媒體型別,但它們並沒有太多的語義。它們只描述了文件中使用的整體語法。應使用(或通過供應商媒體型別擴充套件 ) 支援 HATEOAS 要求的更專業的媒體型別 ,例如:

客戶端通過將 Accept 標頭新增到其請求中來告知伺服器它理解哪些媒體型別,例如:

Accept: application/hal+json

如果伺服器無法以這種表示形式生成所請求的資源,則返回 406(不可接受) 。否則,它會在儲存所表示資源的響應的 Content-Type 標頭中新增媒體型別,例如:

Content-Type: application/hal+json

無國籍的>有狀態的

為什麼?

有狀態伺服器意味著客戶端會話儲存在伺服器例項本地儲存中(幾乎總是儲存在 Web 伺服器會話中)。嘗試水平擴充套件時,這開始出現問題 :如果在負載均衡器後面隱藏多個伺服器例項,則在登入時首先將一個客戶端分派到例項#1 ,然後在獲取受保護資源時將其分配給例項#2 然後第二個例項將以匿名方式處理請求,因為客戶端會話已在本地儲存在例項#1 中

已經找到解決方案來解決這個問題(例如,通過配置會話複製和/或粘性會話 ),但 REST 架構提出了另一種方法:只是不要使伺服器有狀態,使其成為無狀態根據菲爾丁的說法

從客戶端到伺服器的每個請求都必須包含理解請求所需的所有資訊,並且不能利用伺服器上任何儲存的上下文。因此,會話狀態完全保留在客戶端上。

換句話說,無論是否將排程分派給例項#1例項#2 ,都必須以完全相同的方式處理請求。這就是無狀態應用程式被認為更容易擴充套件的原因

怎麼樣?

常見的方法是基於令牌的身份驗證 ,尤其是時尚的 JSON Web 令牌 。請注意,JWT 仍然存在一些問題,特別是關於失效自動延長到期時間 (即記住我的功能)。

旁註

使用 cookie 或標頭(或其他任何東西)與伺服器是有狀態還是無狀態無關:這些只是用於傳輸令牌的媒體(有狀態伺服器的會話識別符號,JWT 等),僅此而已。

當 RESTful API 僅供瀏覽器使用時,( HttpOnly安全 )cookie 可以非常方便,因為瀏覽器會自動將它們附加到傳出請求。值得一提的是,如果你選擇 cookie,請注意 CSRF (一種防止它的好方法是讓客戶端生成並在 cookie 和自定義 HTTP 頭中傳送相同的唯一祕密值 )。

具有條件請求的可快取 API

隨著 Last-Modified 標頭

伺服器可以為包含可快取資源的響應提供 Last-Modified 日期標頭 。然後,客戶端應將此日期與資源一起儲存。

現在,每次客戶請求 API 讀取資源時,他們都可以向他們的請求新增包含他們收到和儲存的最新 Last-Modified 日期的 If-Modified-Since 標頭 。然後,伺服器將比較請求的標頭和資源的實際上次修改日期。如果它們相等,則伺服器返回 304(未修改) ,空體:請求客戶端應使用它具有的當前快取資源。

此外,當客戶端請求 API 更新資源(即使用不安全的動詞)時,他們可以新增 If-Unmodified-Since 標頭 。這有助於處理競爭條件:如果標題和實際的最後修改日期不同,則伺服器返回 412(PRECONDITION FAILED) 。然後,客戶端應在重試修改資源之前讀取資源的新狀態。

隨著 ETag 標頭

一個的 ETag (實體標籤)為資源的特定狀態的識別符號。它可以是用於強驗證的資源的 MD5 雜湊,或用於弱驗證的域特定識別符號。

基本上,該過程與 Last-Modified 標頭相同:伺服器為儲存可快取資源的響應提供 ETag 標頭 ,然後客戶端應將此識別符號與資源一起儲存。

然後,客戶端在想要讀取資源時提供 If-None-Match 標頭 ,其中包含他們收到和儲存的最新 ETag。如果標頭與資源的實際 ETag 匹配,則伺服器現在可以返回 304(NOT MODIFIED)

同樣,客戶端可以在想要修改資源時提供 If-Match 標頭 ,如果提供的 ETag 與實際 ETag 不匹配,則伺服器必須返回 412(PRECONDITION FAILED)

補充說明

ETag>約會

如果客戶在其請求中同時提供日期和 ETag,則必須忽略該日期。來自 RFC 7232( 此處此處 ):

如果請求包含 If-None-Match / If-Match 頭欄位,則接收者必須忽略 If-Modified-Since / If-Unmodified-Since; If-None-Match / If-Match 中的條件被認為是 If-Modified-Since / If-Unmodified-Since 中條件的更準確的替代,並且兩者僅為了與可能不實施 If-None-Match / If-Match 的舊中間人互操作而組合。

淺 ETags

此外,雖然很明顯上次修改日期與資源伺服器端一起保留,但 ETag 可以使用多種方法

通常的方法是實現淺 ETag:伺服器處理請求,好像沒有給出條件頭,但僅在最後,它生成它將要返回的響應的 ETag(例如通過雜湊),並比較它與提供的一個。這相對容易實現,因為只需要一個 HTTP 攔截器(並且許多實現已經存在,具體取決於伺服器)。話雖這麼說,值得一提的是這種方法可以節省頻寬而不是伺服器效能

一個更深入地執行 ETag 的機制可能會提供更大的好處 -如從快取服務的一些請求,並沒有在所有執行計算 -但實現起來最肯定不會那麼簡單,也不是可插拔的淺方法這裡描述。

常見的陷阱

我為什麼不把動詞放在 URL 中?

HTTP 不是 RPC使 HTTP 與 RPC 顯著不同的原因是請求被定向到資源。畢竟,URL 代表統一資源定位器,URL 代表 URI :統一資源 Idenfitier。 URL 以你要處理的資源為目標,HTTP 方法指示你要對其執行的操作**。** HTTP 方法也稱為動詞 :URL 中的動詞使得沒有意義。請注意,HATEOAS 關係也不應包含動詞,因為連結也是針對資源的。

如何部分更新資源?

由於 PUT 請求要求客戶端使用更新的值傳送整個資源,因此 PUT /users/123 不能用於簡單地更新使用者的電子郵件。正如 William Durand 在 Please 中所解釋的那樣 不要像白痴一樣修補。 ,提供了幾種符合 REST 的解決方案:

  • 公開資源的屬性並使用 PUT 方法傳送更新的值,因為 PUT 規範 通過將具有與較大資源的一部分重疊的狀態的單獨標識的資源作為目標來指示部分內容更新是可能的
PUT https://example.com/api/v1.2/users/123/email
body:
  new.email@example.com
  • 使用 PATCH 請求,該請求包含一組描述資源必須如何修改的指令(例如,遵循 JSON 補丁 ):
PATCH https://example.com/api/v1.2/users/123
body:
  [
    { "op": "replace", "path": "/email", "value": "new.email@example.com" }
  ]
PATCH https://example.com/api/v1.2/users/123
body:
  {
    "email": "new.email@example.com"
  }

那些不適合 CRUD 操作世界的行為呢?

引用 Vinay Sahni 設計實用 RESTful API 的最佳實踐

這是事情變得模糊的地方。有很多方法:

  1. 將操作重組為顯示為資源欄位。如果操作不採用引數,則此方法有效。例如,啟用動作可以對映到布林 activated 欄位,並通過 PATCH 更新到資源。

  2. 將其視為具有 RESTful 原則的子資源。例如,GitHub 的 API 可以讓你出演一個要點PUT /gists/:id/star星標DELETE /gists/:id/star

  3. 有時你真的無法將動作對映到合理的 RESTful 結構。例如,多資源搜尋實際上沒有意義應用於特定資源的端點。在這種情況下,即使它不是資源,/search 也會最有意義。這沒關係 - 只需從 API 使用者的角度做正確的事情,並確保明確記錄以避免混淆。

常見做法

https://example.com/api/v1.2/blogs/123/articles
                        ^^^^
https://example.com/api/v1.2/blogs/123/articles
                             ^^^^^     ^^^^^^^^
  • 網址使用 kebab-case (單詞是小寫的,以破折號分隔):
https://example.com/api/v1.2/quotation-requests
                             ^^^^^^^^^^^^^^^^^^
  • HATEOAS 提供資源的 自我連結 ,以自己為目標:
{
  ...,
  _links: {
    ...,
    self: { href: "https://example.com/api/v1.2/blogs/123/articles/789" }
    ^^^^
  }
}
  • HATEOAS 關係使用 lowerCamelCase(單詞是小寫的,然後大寫,除了第一個,省略空格),以允許 JavaScript 客戶端在訪問連結時尊重 JavaScript 命名約定時使用點表示法
{
  ...,
  _links: {
    ...,
    firstPage: { "href": "https://example.com/api/v1.2/blogs/123/articles?pageIndex=1&pageSize=25" }
    ^^^^^^^^^
  }
}