RESTful 最佳實踐
除了傳統對于遠程調用的需求,近來移動開發對于api的規范化需要,restful作為一個流行的接口調用方式,值得深入了解。
聲明 本文屬于轉載:原文
此文為實踐總結,是自己在實踐過程中積累的經驗和"哲學"。部分內容參考相關資料,參考內容請看尾頁。建議對RESTful有一定了解者閱讀!
"哲學"
不要為了RESTful而RESTful
在能表達清楚的情況下,簡單就是美
接口路徑設計
接口設計原則
URI指向的是唯一的資源對象
示例: 指向ID為cloud.mario的Account對象
GET http://~/$version/accounts/cloud.mario
URI可以隱式指向唯一的集合列表
示例: 隱式地指向trades list 集合
GET http://~/$version/trades/(list)
等同于
GET http://~/$version/trades
聚合資源必須通過父級資源操作
示例: Profile
是User
的聚合資源,User
有一個唯一且私有的Profile
資源,只能通過User
操作Profile
。
更新user_id為123456的Profile資源 PUT http://~/$version/users/123456/profiles Request Body:{ "full_name": "cloud.mario", "state": "Beijing", "title": "一個開發者" }
Http Methods
HTTP Operation | Description |
---|---|
GET | 獲取,查找 |
POST | 新增創建 |
PUT | 更新 |
PATCH | 部分更新 |
DELETE | 刪除 |
URL組成
網絡協議(HTTP, HTTPS)
服務器地址
版本
接口名稱
?參數列表
GET https://github.com/v1/trades
為什么需要版本?
當服務被更多其他系統使用的時候,服務的可用性和上下兼容變得至關重要。被外部系統依賴的服務在升級時是一個非常麻煩的事情,既要發布新的接口,又要保留舊的接口留出時間讓調用者去升級。在URL中加入Version標示能很好地解決上下兼容(新老版本共存)問題。
示例1: URL中新增了Path parameter
v1版本
GET http://~/v1/trades?user_id=123456
v2版本
GET http://~/v2/:user_id/trades
示例1中的user_id
參數在v2版本被加入到path parameter中,使用$version
保證了v1
和v2
接口的共存。
示例2: 數據接口發生變化
v1版本
GET http://~/v1/accounts/cloud.mario Response Body: { "user_name": "cloud.mario", "e_mail": "cloud.mario@gmail.com", "state": "Beijign", "title": "一個開發者" }
v2版本
GET http://~/v2/accounts/cloud.mario Response Body: { "user_name": "cloud.mario", "e_mail": "cloud.mario@gmail.com", "profile": { "state": "Beijign", "title": "一個開發者" } }
示例2中的接口返回數據結構已經發生了變化。使用$version
保證了v1
和v2
接口的共存。
URL定義限制
不使用大寫字母
使用中線-代替下劃線_
參數列表應該被encode過
接口分類
資源對象的CURD操作
GET http://~/$version/trades #獲取trades列表 GET http://~/$version/trades/:id #根據id獲取單個trade POST http://~/$version/trades #創建trade PUT http://~/$version/trades/:id #根據id更新trade PATCH http://~/$version/trades/:id #根據id部分更新trade DELETE http://~/$version/trades/:id #根據id刪除trade
系統設置
使用settings
標識,根據服務的屬性選擇http方法。
http://~/settings/$version/server-name
示例1: 搜索
GET http://~/services/$version/search?q=filter&category=file
示例2: 任務隊列操作
PUT http://~/services/$version/queued/jobs 往任務隊列里面添加一個新的任務 DELETE http://~/services/$version/queued/jobs/:id 根據id刪除任務
示例3: 更改界面語言環境
PUT http://~/settings/$version/gui/lang { "lang": "zh-CN"}
為什么需要區分?
Microservices
是一個全新的概念,它主要的觀點是將一個大型的服務系統分解成多個微型系統。每個微型系統都能獨立工作,并且提供各種不同的服務。獨立運行的特點使微型系統之間不會產生相互影響,其中的一個微型系統宕機并不會牽連到其他的微型系統。這種架構使分布式系統的節點數量大大提升。因為RESTful服務是無狀態的,所以這種分解并不會帶來狀態共享的問題。
路由規則(邏輯)
當我們需要對不同屬性的接口做路由規則的時候,按功能劃分接口是一個很好的方案。例如:我們要對系統設置接口設置增加更嚴格的調用限制。
緩存
網絡接口相對于堆棧接口來說數據傳輸極其不穩定,盡可能地減少數據傳輸不僅能控制這種風險還能減少流量。使用緩存還能有效地提高后臺的吞吐量。
后臺在響應請求時使用響應頭E-Tag
或Last-Modified
來標記數據的版本,前臺在發送請求時將數據版本通過請求頭If-Match
幫助后臺判斷緩存的使用。
Request Header
If-Match: 2390239059405940
Response Header
E-Tag: 2390239059405940Last-Modified: 1403183502701
Bookmarker
在實際的環境中,有大量的查詢需求是相同的。將這些搜索需求標簽化能降低使用難度也可以達到重用的目的。
示例: 查找狀態為關閉的訂單
普通方式
GET http://~/$version/trades?status=closed&sorting=-created_at
Bookmarker
GET http://~/$version/trades#recently_closed
或
GET http://~/$version/trades/recently_closed
HATEOAS
HATEOAS通過Web Linking的方式來描述程序的狀態信息
Link 主要包含以下屬性:
Property | Description |
---|---|
rel | 關聯內容 |
href | URL |
type | 媒體類型 |
method | Http Method |
title | 標題 |
arguments | 參數列表 |
value | 返回值 |
Rel
可能為以下值:
Value | Description |
---|---|
next | 下一步 |
prev | 上一步 |
first | 第一步,最前 |
last | 最后一步,最后 |
source | 來源 |
self | 資源自身,相對于this |
Web Linking 可以通過兩種方式傳遞至客戶端:
Http Header
Link: <http://~/$version/trades?page_no=10>; rel="next", <http://~/$version/trades?page_no=19>; rel="last"
Http JSON Body
{ "links": [ { "rel": "next", "href": "http://~/$version/trades?page_no=1" }, { "rel": "last", "href": "http://~/$version/trades?page_no=19" } ] }
示例1: 用戶注冊業務
用戶填寫E-Mail與密碼
完善用戶資料
Register Request
POST http://~/$version/accountsHeaders: Accept: application/json Content-Type: application/json;charset=utf-8Body: { "username": "cloud.mario@gmail.com", "e_mail": "cloud.mario@gmail.com", "password": "balabala" }
Register Response
Headers: Content-Type: application/json;charset=utf-8 Status: 201 Created Body: { "uri": "http://~/$version/accounts/cloud.mario", "identity": "cloud.mario", "created_at": 1403535668653, "links": [ { "rel": "next", "href": "http://~/$version/accounts/cloud.mario/profiles", "method": "POST", "title": "Editing Profiles", "arguments": "status=editing" } ] }
Profile Request
POST http://~/$version/accounts/cloud.mario/profilesHeaders: Accept: application/json Content-Type: application/json;charset=utf-8Body: { "full_name": "cloud.mario", "state": "Beijing", "title": "一個開發者" }
Profile Response
Headers: Content-Type: application/json;charset=utf-8Status: 201 Created Body: { "uri": "http://~/$version/accounts/cloud.mario/profiles", "identity": "cloud.mario", "created_at": 1403535668653 }
示例2: 請看下節<分頁>
HATEOAS在解決什么問題?
HATEOAS是Hypermedia as the Engine of Application State的縮寫形式,中文意思為:超媒體應用狀態引擎。它的核心思想是使用超媒體表達應用狀態,與hypertext-driven思想是一致的。在此之前,我們大多數的程序業務控制在前臺完成。例如:我們會在前臺做注冊流程,我們在前臺判定下一步應該做什么,可以做什么。當使用HATEOAS時,這些狀態流程控制都在應用程序的后臺完成。我們使用超媒體來表達前臺做完某一步驟之后可以做哪些? 這樣一來,前臺的任務就變得相當簡單了,前臺需要處理的是理解狀態表述,數據收集和結果顯示。
思考
HATEOAS會帶來怎樣的改變? 使用它的意義在哪?
分頁
Request
GET http://~/$version/trades?page=10&pre_page=100
Response
Link Header
Link: <http://~/$version/trades?page=11&pre_page=100>; rel="next", <http://~/$version/trades?page=19&pre_page=100>; rel="last"
JSON Body
{ "links": [ { "rel": "next", "href": "http://~/$version/trades?page=11&pre_page=100" }, { "rel": "last", "href": "http://~/$version/trades?page=19&pre_page=100" } ] }
安全
調用限制
為保證服務的可用性應對服務進行調用過載保護
Response Headers
X-RateLimit-Limit: 3000 調用量的最大限制 X-RateLimit-Reset: 1403162176516 調用限制重置時間 X-RateLimit-Remaining: 299 剩余的調用量
安全驗證
RESTful服務使用Oauth2的方式進行調用授權,使用http請求頭Authorization
設置授權碼; 必須使用User-Agent
設置客戶端信息, 無User-Agent
請求頭的請求應該被拒絕訪問。
Request Header
User-Agent: Data-Server-Client Authorzation: Bearer 383w9JKJLJFw4ewpie2wefmjdlJLDJF
為什么建議使用Oauth2授權?
Oauth2的參與者為:客戶端,資源所有者,授權服務器,資源服務器??蛻舳讼葟馁Y源所有者得到授權碼之后使用授權碼從授權服務器得到token
,再使用token
調用資源服務器獲取經過資源所有者授權使用的資源。這種授權方式的特點有:
資源所有者可以隨時撤銷授權許可
可以通過撤銷
token
拒絕客戶端的調用資源服務器可以拒絕客戶端的調用
通過這三種方式可以做到對資源的嚴格保護。資源的訪問權限也把握在資源所有者的手中,而不是資源服務器。
當然,Oauth2
授權框架也允許受信任的客戶端直接使用token調用資源服務器獲取資源。這種靈活性完全取決于客戶端類型和對資源的保護程度。
為什么授權碼要放在Http Header中?
WEB服務器對訪問做記錄已經成為了行業的一個標準,訪問記錄不僅可以用來做訪問量統計還能用來做訪問特征分析?;ヂ摼W廣告平臺就是利用訪問記錄來做精準營銷的。如果
token
(授權碼)包含在URL中就有很大的安全風險。包含在URL中的
token
串可能被進行重定向傳遞。通過這兩種方式入侵者可以不通過授權而使用泄漏的授權碼訪問那些受保護的數據,會造成數據泄漏的風險。
以Apache為例,訪問日志為:
127.0.0.1 - - [24/Jun/2014:14:38:04 +0800] "GET /v1/accounts/cloud.mario?token=dgdreLJLJLER798989erJKJK HTTPS/1.1" 200 343
通過對訪問日志的提取,很容易得到token
信息。
數據設計
交互原則
查詢,過濾條件使用query string。
用來描述數據或者請求的元數據放Header中,例如
X-Result-Fields
。Content body 僅僅用來傳輸數據。
數據要做到拿來就可用的原則,不需要“拆箱”的過程。
結構
使用JSON格式傳輸數據,在http請求頭和響應頭申明Content-Type
。返回的數據結構應該做到盡可能簡單,不要過于包裝。響應狀態應該包含在響應頭中!
Request
Accept: application/json Content-Type: application/json;charset=UTF-8
Response
Content-Type: application/json;charset=UTF-8
錯誤的做法
{ "status": 200, "data": { "trade_id": 1234, "trade_name": "Bala bala" } }
正確的做法
Response Headers: Status: 200 Response Body: { "trade_id": 1234, "trade_name": "Bala bala" }
示例1: 創建User對象
POST http://~/$version/users Request headers: Accept: application/json Content-Type: application/json;charset=UTF-8 body: { "user_name": "Cloud Mario" } Response status: 201 Created headers: Content-Type: application/json;charset=UTF-8 body: { "uri": "http://~/$version/users/1234", "identity": 1234, "created_on": "Date()", "links": [ { "rel": "next", "href": "http://~/gui/users/1234" } ] }
為什么是JSON?
JSON 是一種可以跨平臺高擴展的輕量級的數據交換格式。易于人閱讀和編寫,同時也易于機器解析和生成。
屬性定義限制
不能使用大寫(大小寫友好)
使用下劃線_命名(連接兩個單詞)
屬性和字符串值必須使用雙引號""
提取部分字段
無狀態服務器應該允許客戶端對數據按需提取。在請求頭使用X-Result-Fields
指定數據返回的字段集合。
例如:trade 有trade_id,
trade_name
, created_at
三個屬性,客戶端只需其中的trade_id
與trade_name
屬性。
Request Header
X-Result-Fields: trade_id,trade_name
子對象描述
數據里面的子對象使用URI描述不應該被提取,除非用戶指定需要提取子對象
示例: trade
里面的order
對象
錯誤的做法
{ "trade_id": "123456789", "full_path": null, "order": { "order_id": "987654321" } }
正確的做法
{ "trade_id": "123456789", "order": "http://~/$version/orders/987654321" }
應用指定提取子對象,需要在請求頭聲明X-Expansion-Fields
Request
X-Expansion-Fields: true
為什么要客戶端指定提取子對象時才提取?
懶模式服務能夠最大程度地節省運算資源。雖然與客戶端交互的次數有所增加,但是能做到按需提取,按需響應,這也是響應式設計的一大特點。客戶端的用戶行為模式無法真實地模擬,也就無法確定哪些資源需要做到一次性推送,讓客戶端按需使用是一個不錯的方式。
關于空字段
應該在返回結果里面剔除空字段,因為null值傳輸到客戶端并沒有實際的含義,反而增加了占用空間。
Tips
使用HTTP Header時,優先使用合適的標準頭屬性。用X-
作為前綴自定義一個頭屬性,例如: X-Result-Fields
。
狀態碼&錯誤處理
應用狀態碼
Code | HTTP Operation | Body Contents | Description |
---|---|---|---|
200 | GET,PUT | 資源 | 操作成功 |
201 | POST | 資源,元數據 | 對象創建成功 |
202 | POST,PUT,DELETE,PATCH | N/A | 請求已經被接受 |
204 | DELETE,PUT,PATCH | N/A | 操作已經執行成功,但是沒有返回數據 |
301 | GET | link | 資源已被移除 |
303 | GET | link | 重定向 |
304 | GET | N/A | 資源沒有被修改 |
400 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 參數列表錯誤(缺少,格式不匹配) |
401 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 未授權 |
403 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 訪問受限,授權過期 |
404 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 資源,服務未找到 |
405 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 不允許的http方法 |
409 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 資源沖突,或者資源被鎖定 |
415 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 不支持的數據(媒體)類型 |
429 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 請求過多被限制 |
500 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 系統內部錯誤 |
501 | GET,PSOT,PUT,DELETE,PATCH | 錯誤提示(消息) | 接口未實現 |
容器狀態碼
容器狀態碼是指http容器的狀態碼,應用不應該使用或限制使用
Code | HTTP Operation | Body Contents | Description |
---|---|---|---|
303 | GET | link | 靜態資源被移除,應用限制使用 |
503 | GET,PSOT,PUT,DELETE,PATCH | text body | 服務器宕機 |
Tips
4開頭的錯誤用來表達來自于客戶端的錯誤,例如: 未授權,參數缺失。5開頭的錯誤用來表達服務端的錯誤,例如: 在連接外部系統(DB)發生的IO錯誤。
錯誤信息格式
錯誤信息應該包含下列內容:
錯誤標題
message
, 必須錯誤代碼
error code
, 必須錯誤信息
error message
, 必須資源
resource
, 可選屬性
field
, 可選文檔地址
document
, 可選
Tips
Error Code
盡可能做到簡潔明了,提取異常的關鍵字并且使用下劃線_把它們連接起來。
示例: 調用頻率超過限制,Response:
Headers: Content-Type: application/json;charset=UTF-8 X-RateLimit-Limit: 3000 X-RateLimit-Reset: 1403162176516 X-RateLimit-Remaining: 0 { "message": "Message title", "errors": [ { "code": "rate_limit_exceeded", "message": "Too Many Requests. API rate limit exceeded", "document": "https://developer.github.com/v3/gists/" } ] }
錦上添花
格式化(Pettyprint)JSON數據(返回結果)并且使用gzip壓縮,Pettyprint易于閱讀,多余的空格在經過gzip壓縮之后占用空間比壓縮之前更小。
重寫
Server
頭返回
X-Powered-By
Response Headers
X-Pretty-Print: true Content-Encoding: gzip Server: cloud.mario@sina.com X-Powered-By: cloud.mario;email=cloud.mario@gmail.com
資源福利
框架&工具
參考資料
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
https://developer.yahoo.com/social/rest_api_guide/http-response-codes.html
http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
posted on 2014-08-18 08:47 paulwong 閱讀(6476) 評論(0) 編輯 收藏 所屬分類: RESTFUL-API