RESTful Service API 常見問題解決方案


REST 風格的優秀設計應該像下面這些

- GET /users 獲取所有用戶
- GET /users/1234 獲取ID為1234的用戶
- POST /users 創建一個新用戶
- PUT /users/1234 更新ID為1234的用戶
- PATCH /users/1234 更新ID為1234的用戶的部分內容
- DELETE /users/1234 刪除ID為1234的用戶

如果要設計一個資源擁有另外一個資源的情況的API,例如,設計一個包含用戶(users)和用戶的評論(comments)的 API 可以采用這樣的形式:
- GET /users/1234/comments 獲取用戶ID為1234的所有評論
- GET /users/1234/comments/1 獲取用戶ID為1234的評論ID為1的單個評論
- DELETE /users/1234/messages/1 刪除用戶評論ID為1,屬於用戶1234的單個評論
 
實際工程實踐中往往會遇到並不是對一個資源簡單的 CRUD 的場景,設計此類 API 有這些手法可供參考
  • 將這些操作變成一個資源的屬性,比如 disable 一個 user,可以在 user 里面加一個 disabled 的屬性,可以設計一個 API 使用 PATCH /users/1234 將 disabled 設置成 true 即可。
  • 將這個操作看成某個資源的附屬資源(就像上面例子中的 comments 一樣)來設計,比如GitHub的Star a gist API ,就是這樣的,它把star操作放在這個資源的后面,看上去好像是一個附屬資源:- PUT /gists/:id/star   - DELETE /gists/:id/star
  • 在不得不使用其它例外形式設計 API 時,盡量用文檔寫清楚輸入輸出和返回值等其他必要信息,避免讓習慣了使用資源名的調用者感到困惑
考慮到系統迭代和兼容性,需要在 API 中引入版本規則
  一種是將版本號放在 http header 內,另一種是直接放在 URL 中。而放在 URL 中是最常見的做法
    GET https://api.twitter.com/1.1/friends
    GET "https://graph.facebook.com/v2.8/me
如果一個 API 的版本過期了,任何把該請求重定向到最新版本上。比如 user API v1 版本過期了,當有調用/api/v1.0/users/1234的時候,應該被重定向(http 30x)到最新的 /api/v2.0/users/1234 上。
優雅的設計條件過濾,排序,搜索等傳入參數形式
  RESTful API 經常有對返回數據過濾和排序的要求,這些輸入參數推薦采用 HTTP Query Parameter 的方式實現。
  • 比如你要設計一個API,返回所有已經登錄的用戶,可以這樣做:GET /users?login=true
  • 獲取所有的用戶,返回結果按照create_at降序排序可以這樣設計:GET /users?sort=-create_at
  • 組合使用過濾條件和排序,GET /users?sort=-create_at,login_at&login=true 表示返回所有已登錄用戶,結果按照create_at降序, login_at升序
  • 單獨為 API 設計一個 Query Parameter 專門用於搜索,從 API 中傳遞過來的 Query Parameter 可以直接設置成這些搜索框架的輸入條件GET /users?q=key&&sort=-create_at,login_at&diabled=false
  • 映射到一個新的API(相當於快捷方式)比如設計一個用於返回最近登錄用戶的API:GET /users/recently_login這種設計可以簡化客戶端的調用,否則調用者每次都要根據時間合成 Query Parameter,增加了客戶端使用復雜度
  • 查詢數據的部分內容GET /user?fields=id,user_name,address&diabled=false&sort=-login_at   GET /facebook/v2.8/me?fields=id,name,birthday,cover,devices,email&access_token=xxX

API中都使用了下划線(user_name)的形式來命名這些參數,使用划線(user_name)還是使用駝峰(userName)的形式?下划線分割的形式比使用駝峰的形式更容易閱讀(容易20%)

合理設計返回數據的形式,格式和考慮啟用壓縮(gzip)

  假如有個系統提供一個 API 用於上傳一張圖,這張圖上傳之后你可以調用另外一個 API 修改這個圖片的描述。如果調用上傳 API 后,返回數據中沒有返回這張圖的唯一性 ID,你就無法接着調用其它 API 引用到這個圖的資源,從而無法進行修改描述的操作,除非之前額外再次調用查詢操作拉取到這張圖唯一性 ID。

  通常,POST 操作成功以后,我們一般也把新創建的資源的 URL 放在 HTTP header 的 location 字段中,方便客戶的拉取。例如上上樹圖片上傳的 API 返回的 header 中可以包含location: http://api.domain.name/photos/1234

  RESTful API 一般都是返回文本數據,啟用 gzip 通常可以節省60%-80%以上的帶寬(這個數據很好證明,隨便使用幾個個 json 文件 gzip下就可以看出來,我測試幾個 json 文件一般300K左右都能被壓縮成50K左右),尤其是在返回的數據比較大情況下,壓縮比更高。不過啟用gzip 不可避免會增加 CPU 的負擔,實際工程項目中需要權衡考量。

  至於到底用什么用的格式來返回數據?XML?JSON?純文本?但從統計數據來看 JSON 格式目前是使用做多的 REST API 的輸入輸出格式。

根據不同的 API 操作,設置合適的 HTTP 狀態碼和必要的出錯信息

  • 200 OK 用於返回 GET, PUT, PATCH 或 DELETE 的操作。有使用也用來返回沒有創建數據的 POST 操作;
  • ** 201 Created** 用來返回 POST 操作並且成功創建了數據的情況。新創建的數據資源的鏈接應該放在location中返回;
  • 204 No Content 用來返回一次成功的請求,但是該請求返回的 body 為空的情況,如 DELETE 請求;
  • 304 Not Modified 表示緩存沒有失效,和上次的請求相比,沒有新的內容;
  • 400 Bad Request 用於返回 API 參數不正確的情況,比如傳入的 JSON 格式錯誤無法解析等;
  • 401 Unauthorized 用於表示請求等 API 缺少身份驗證信息;
  • 403 Forbidden 用於表示該資源不允許特定用戶訪問;
  • 404 Not Found 請求一個不存在的資源;
  • 429 Too Many Requests 請求過於頻繁,可以用在客戶端調用過於頻繁的情況。

使用 token 機制設計鑒權和驗證系統(Authorization and Authentication)

  常見的場景就是用戶系統-結合 OAuth2,參考騰訊雲微視頻MVS API,這里給出一個實用的解決方案:

  • 用戶使用戶名密碼或者第三方登錄,最終請求一個我們設計的登錄 API(這個 API 接受用戶名密碼,或第三方登錄驗證結果);
  • 服務端認證成功以后,生成一個 token,並將這個 token 和用戶信息關聯在一起,同時返回這個 token 給調用客戶端;
  • 客戶端記錄並保存下這個 token;
  • 下次客戶端發起和用戶相關請求 API 都要在 http header 中帶上這個 token;
  • 服務端通過這個 token 去區分用戶是誰,判斷這個用戶是否已經登錄和有什么樣的權限;
  • 服務端也要考慮 token 的失效時間;
  • 客戶端在發現 token 失效的時候重新請求新的 token

為什么要多一個步驟使用 token 呢?為什么不直接把用戶名和密碼放在 http header 中直接做授權和驗證?原因是調用 API 一般會被頻繁調用,這樣用戶名和密碼頻繁在網絡上傳輸,增加了泄漏的危險。如果使用token,即使泄漏了也不會暴露用戶的密碼,何況 token 也被經常被設計成有時間限制的,超時以后當前 token 就會失效,需要客戶端重新做驗證獲得新的 token,暴露之后的影響很快就會過去。
其實獲取 token,用 token 做授權和驗證和 OAuth 2 如出一轍,手法完全相同的,只是 OAuth 2 有更復雜的標准步驟去換取這個token,並且這個 token 的用途不同。OAuth 2 的 token 用來授權給第三方使用,我們自己設計的系統 token 僅限在自己系統本身 API 使用。

如何實現數據的分頁返回
 
一種是使用類似 Facebook Graph API 的方法,它把分頁信息和數據一起返回,調用者只需要再次請求 next 中 URL 就可以獲取下一頁的數據,這種方式優點是靈活和直觀,可以隨意添加和分頁相關的其他屬性,例如總記錄數,總頁數等等。其中cursors用來解決“流”的問題(由於數據是動態增加的,基於舊數據的頁數和頁碼會失效,於是引入 cursors 來標記數據位置,關於這個問題,Twitter 在介紹其timeline API時有

另一種符合WEB標准的做法是使用 link header,簡單來說就是在 http header 使用 link字段,提供一個和超鏈接一樣目的 URL 地址,來實現不同資源之間的轉跳。如GitHub的Api文檔是這樣規定分頁信息的,這種做法缺點是不太直觀,優點是不會干擾數據,返回內容都是數據本身,無需在數據上嵌入額外的屬性來說明分頁信息,簡單干凈

如何處理有關聯資源的返回數據

  對客戶端來說,最直觀和容易處理的返回形式如下:

  返回數據中 avatar 和 name 是每條數據都是重復的,所以你也可以這樣設計返回數據:先返回該用戶的所有評論 /comments?user=1234

  再通過請求該用戶 API 的相關內容 /users/1234:{user_id: "1234", avatar: "a.jpg", nickName:"Jeffrey"...}這種情況下其實可以將依賴資源嵌入返回對象中,避免了客戶端需要再一次發起請求來獲取這個 user 的詳細信息/comments?user=1234 直接返回類似這樣的信息即可:

  

考慮啟用 HTTP 緩存機制

HTTP協議本身支持兩種緩存機制: ETagLast-Modified

  1. ETag:HTTP 請求中在 header 中包含一個內容的 hash,如果返回結果沒有變化,該請求會直接返回304 Not Modified,而不是所有數據內容本身
  2. Last-Modified: 和 Etag 工作原理差不多,只是使用時間戳作為內容是否過期的標志。

限制 API 調用頻次(Rate limiting)

 如果一個客戶端請求 API 的頻率太快,根據HTTP協議,可以返回429 Too Many Requests

如果要為客戶端提供更加詳細的調用頻次和訪問次數之類的信息,除了提供文檔說明以外,還可以在 http header 用自定義字段的形式提供,比如 Twitter API 是這樣做的:
X-Rate-Limit-Limit: 該請求的調用上限
X-Rate-Limit-Remaining: 15分鍾內還可以調用多少次
X-Rate-Limit-Reset: 還有多少秒之后訪問限制會被重置
 
盡可能的使用 HTTPS,涉及用戶驗證的 API 一定要強制啟用 HTTPS
HTTPS 現在已經是各種網絡服務的標配(比如 Xcode 默認不允許請求不安全的 HTTP 信息)
如果你的WEB Server 是 Nginx,在部署了 HTTPS 的情況下,下面兩個選項務必仔細設置,因為這個兩個簡單的設置可以很大程度上避免一些安全問題:
  
  • ssl_prefer_server_ciphers: 表示服務端加密算法優先於客戶端加密算法,主要是防止降級攻擊 (downgrade attack)
  • Strict-Transport-Security(HSTS):告訴瀏覽器這個域名在指定的時間(max-age)內應該強制使用 HTTPS 訪問。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM