RESTful API 最佳實踐

阮一峰發表於2018-10-03

在參考了GitHub API設計和大量部落格文章後總結了一下RESTful API的設計,分享如下。想要更好的理解RESTful API首先需要理解如下概念:

REST:REST(Representational State Transfer)這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的,翻譯成中文大意為表現層狀態傳輸。由於他是HTTP協議(1.0版和1.1版)的主要設計者、Apache伺服器軟體的作者之一、Apache基金會的第一任主席,所以REST原則迅速流行起來。當一個軟體架構符合REST原則,我們稱之為RESTful架構。說了這麼多,我們為什麼要使用RESTful架構?使用RESTful架構有什麼好處?因為按照RESTful架構可以充分的利用HTTP協議帶給我們的各種功能,算是對HTTP協議使用的最佳實踐,還有一點就是可以使軟體架構設計更加清晰,可維護性更好,但是並不是所有情況都需要完全遵守REST原則,畢竟實際情況遠遠比REST原則所定義的更加複雜,下面會詳細介紹。

冪等性:冪等性(Idempotence)本身是一個數學概念,在HTTP/1.1規範中冪等性的定義是:

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

翻譯過來大意就是如果方法呼叫一次和多次產生額外的效果是相同的,它就具有冪等性。

例子:在HTTP中使用GET方法通常用於從伺服器獲取資源,無論呼叫多少次產生的額外效果都是從伺服器獲取資源,所以GET具有冪等性;而POST方法通常用於提交資料在伺服器上建立一個資源,由於最終建立的結果每次都是不同的,所以POST不具有冪等性;但是PUT方法卻是冪等的,因為每次呼叫產生的效果都是對資源進行更新。

安全方法:安全方法是指不修改資源的 HTTP 方法。譬如,當使用 GET 或者 HEAD 作為資源 URL,都必須不去改變資源。然而,這並不全準確。意思是:它不改變資源的 表示形式。對於安全方法,它仍然可能改變伺服器上的內容或資源,但這必須不導致不同的表現形式。

有關HTTP常用方法冪等性和安全性如下:

RESTful API設計規則:

1. URI

  • 應該將API部署在專用域名之下:https://api.example.com
  • 不用大寫
  • 用中槓-不用下槓_;
  • 引數列表要encode;
  • URI中不應該出現動詞,動詞應該使用HTTP方法表示,但是如果無法表示,也可使用動詞,例如:search沒有對應的HTTP方法,可以在路徑中使用search,更加直觀;
  • URI中的名詞表示資源集合,使用複數形式;
  • 雖然/在URI中表達層級,但是避免為了追求REST導致層級過深,適當使用參數列示。
    GET /comments/tid/tid=1&page=1

2. Request:通過標準HTTP方法對資源CRUD

  • GET:查詢資源
    GET /comments //獲取所有評論 GET /comments/tid/1 //獲取文章tid為1的所有評論
  • POST:建立資源
    POST /comments/tid/1 //為tid為1的文章建立評論
  • PUT:更新資源
    PUT /comments/cid/like/1 //為cid為1的評論點贊
  • DELETE:刪除資源
    DELETE /comments/cid/1 //刪除cid為1的評論

3. Response

  • 採用JSON,不要使用XML
  • 預設情況下JSON外層不需要巢狀大括號,API需要支援JSONP跨域訪問或者客戶端無法訪問HTTP Header才需要加上巢狀大括號
  • 預設情況下不要過濾API輸出中的空格,並且要支援gzip

4. API版本控制

  • 在URI中存放:GET /v1/comments;
  • 客戶端在Accept Header中存放:Accept: application/vnd.github.v3+json,伺服器自定義Header返回當前版本資訊:X-GitHub-Media-Type: github.v3; format=json(GitHub在用);
  • 以上兩種方法根據情況選擇,Github用的方式是REST中所要求的方式;
  • 測試API和正式API要進行區分,方式通過如上兩種方式實現。

5. 速度限制

為了避免請求氾濫,給API設定速度限制很重要。為此 RFC 6585 引入了HTTP狀態碼429(too many requests)。加入速度設定之後,應該提示使用者,至於如何提示標準上沒有說明,不過流行的方法是使用HTTP的返回頭。
下面是幾個必須的返回頭(依照twitter的命名規則):

  • X-Rate-Limit-Limit :當前時間段允許的併發請求數
  • X-Rate-Limit-Remaining:當前時間段保留的請求數。
  • X-Rate-Limit-Reset:當前時間段剩餘秒數

為什麼使用當前時間段剩餘秒數而不是時間戳?

時間戳儲存的資訊很多,但是也包含了很多不必要的資訊,使用者只需要知道還剩幾秒就可以再發請求了這樣也避免了clock skew問題。

6.快取

HTTP提供了自帶的快取框架。你需要做的是在返回的時候加入一些返回頭資訊,在接受輸入的時候加入輸入驗證。基本兩種方法:

  • ETag:當生成請求的時候,在HTTP頭裡面加入ETag,其中包含請求的校驗和和雜湊值,這個值和在輸入變化的時候也應該變化。如果輸入的HTTP請求包含IF-NONE-MATCH頭以及一個ETag值,那麼API應該返回304 not modified狀態碼,而不是常規的輸出結果。
  • Last-Modified:和etag一樣,只是多了一個時間戳。返回頭裡的Last-Modified:包含了 RFC 1123 時間戳,它和IF-MODIFIED-SINCE一致。HTTP規範裡面有三種date格式,伺服器應該都能處理。

7.覆蓋HTTP方法

一些HTTP客戶端只支援GET和POST請求。為了能夠加強這些客戶端的訪問能力,API需要能夠覆蓋HTTP方法。儘管這裡沒有任何強制的標準,但流行的做法是API會接收一個請求頭X-HTTP-Method-Override,它的值可以是PUT、PATCH或者DELETE三者之一。

注意,用來覆蓋HTTP方法的header只能在POST請求中被接受。GET請求永遠不能修改伺服器上的資料。

8.過濾資訊

如果記錄數量很多,伺服器不可能都將它們返回給使用者。API應該提供引數,過濾返回結果。

下面是一些常見的引數:

?limit=10:指定返回記錄的數量
?offset=10:指定返回記錄的開始位置。
?page=2&per_page=100:指定第幾頁,以及每頁的記錄數。
?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序。
?animal_type_id=1:指定篩選條件

就像HTML的出錯頁面向訪問者展示了有用的錯誤訊息一樣,API也應該用之前熟悉易讀的格式來提供有用的錯誤訊息。錯誤的表現形式應該跟其他資源保持一致,只是用一些自己的欄位。

API應該一直返回合理的HTTP狀態碼。API錯誤一般情況下分成兩類:代表客戶端錯誤的400系列狀態碼和代表服務端錯誤的500系列狀態碼。API至少把所有400系列錯誤統一用易讀的JSON格式來展示。如果可能(比如,如果負載均衡和反向代理能夠建立自定義錯誤內容的話),500系列的狀態碼也這麼弄。

JSON錯誤內容應該為開發者提供一些東西 – 有用的錯誤訊息,唯一的錯誤碼(通過它可以在文件中找到更多錯誤細節),可能的話提供錯誤細節描述。用JSON格式來輸出錯誤看起來這樣:

{
  "code" : 1234,
  "message" : "Something bad happened : (",
  "description" : "More details about the error here"
}

對於PUT、PATCH和POST的請求進行的校驗錯誤需要巢狀多個欄位。最佳做法是用固定的錯誤碼來表示校驗失敗,然後在額外的errors欄位中提供錯誤的細節,像這樣:

{
  "code" : 1024,
  "message" : "Validation Failed",
  "errors" : [
    {
      "code" : 5432,
      "field" : "first_name",
      "message" : "First name cannot have fancy characters"
    },
    {
       "code" : 5622,
       "field" : "password",
       "message" : "Password cannot be blank"
    }
  ]
}

10.HTTP狀態碼

HTTP定義了很多有意義的狀態碼,你可以在你的API中使用。這些狀態碼可以幫助API消費者用來路由它們獲取到的響應內容。整理了一個你肯定會用到的狀態碼列表:

  • 200 OK – 對成功的GET、PUT、PATCH或DELETE操作進行響應。也可以被用在不建立新資源的POST操作上
  • 201 Created – 對建立新資源的POST操作進行響應。應該帶著指向新資源地址的Location header)
  • 204 No Content – 對不會返回響應體的成功請求進行響應(比如DELETE請求)
  • 304 Not Modified – HTTP快取header生效的時候用
  • 400 Bad Request – 請求異常,比如請求中的body無法解析
  • 401 Unauthorized – 沒有進行認證或者認證非法。當API通過瀏覽器訪問的時候,可以用來彈出一個認證對話方塊
  • 403 Forbidden – 當認證成功,但是認證過的使用者沒有訪問資源的許可權
  • 404 Not Found – 當一個不存在的資源被請求
  • 405 Method Not Allowed – 所請求的HTTP方法不允許當前認證使用者訪問
  • 410 Gone – 表示當前請求的資源不再可用。當呼叫老版本API的時候很有用
  • 415 Unsupported Media Type – 如果請求中的內容型別是錯誤的
  • 422 Unprocessable Entity – 用來表示校驗錯誤
  • 429 Too Many Requests – 由於請求頻次達到上限而被拒絕訪問

11.認證

RESTful API應該是無狀態。這意味著對請求的認證不應該基於cookie或者session。相反,每個請求應該帶有一些認證憑證。

如果一直使用SSL,認證憑證可以簡單的使用隨機生成的access token,把其做為HTTP Basic Auth中user name欄位的值傳給API。這麼做的好處是可以通過瀏覽器訪問 – 如果瀏覽器從伺服器收到401 Unauthorized狀態碼,它將會彈出一個對話方塊讓人輸出認證憑證。

當然,這種基於token來進行基本認證的方法只能當使用者從API管理後臺拷貝了一個token到自己的程式碼中才行。如果搞不到token,只能使用OAuth 2來把安全token傳遞給第三方。OAuth 2使用Bearer token,並且也是基於SSL來保證傳輸安全。

支援JSONP的API可能需要第三種方法來實現認證,因為JSONP的請求沒法傳送HTTP Basic Auth憑證或者Bearer token。這種情況下,可以使用一個額外的查詢引數access_token。注意:使用查詢引數來傳遞token存在一個固有的安全隱患,因為大多數web伺服器會在伺服器日誌中儲存查詢引數。

不管怎麼樣,以上三種方法是用來在API之間傳輸token的方法。實際傳輸的token可以是一樣的。

12.使用SSL

一定要使用SSL。沒有例外。如今,你的web API可以從任何有網際網路的地方(像圖書館,咖啡館,機場等等)被訪問到。這些地方並不都是安全的。很多地方根本沒有對網路連線進行加密,如果認證憑證被劫持的話,這樣訪問者很容易被竊聽或者被冒充。

一直使用SSL的另一個優勢是,加密的連線簡化了使用者認證的工作 – 你可以使用簡單的access token,而不需要對每個API請求進行簽名。

需要注意的一件事是以非SSL的形式訪問API的URL。不要把請求跳轉到它們的SSL版本上。直接丟擲一個嚴重錯誤!

13.Hypermedia API

RESTful API最好做到Hypermedia,即返回結果中提供連結,連向其他API方法,使得使用者不查文件,也知道下一步應該做什麼。
比如,當使用者向http://api.example.com的根目錄發出請求,會得到這樣一個文件。

{"link": {
  "rel":   "collection https://www.example.com/comments",
  "href":  "https://api.example.com/comments",
  "title": "List of comments",
  "type":  "application/vnd.yourformat+json"
}}

上面程式碼表示,文件中有一個link屬性,使用者讀取這個屬性就知道下一步該呼叫什麼API了。rel表示這個API與當前網址的關係(collection關係,並給出該collection的網址),href表示API的路徑,title表示API的標題,type表示返回型別。
Hypermedia API的設計被稱為HATEOAS。

在進行分頁查詢時可以返回下一頁的URI,如果沒有說明伺服器已經取到最後一條資料了,客戶端可以減少不必要的請求以及URI的構造,建議在分頁的情況下使用。

這就是我總結出來的RESTful API的最佳設計實踐,文章如果有紕漏,歡迎指出。

相關文章