除了傳統(tǒng)對于遠(yuǎn)程調(diào)用的需求,近來移動開發(fā)對于api的規(guī)范化需要,restful作為一個(gè)流行的接口調(diào)用方式,值得深入了解。
聲明 本文屬于轉(zhuǎn)載:原文
此文為實(shí)踐總結(jié),是自己在實(shí)踐過程中積累的經(jīng)驗(yàn)和"哲學(xué)"。部分內(nèi)容參考相關(guān)資料,參考內(nèi)容請看尾頁。建議對RESTful有一定了解者閱讀!
"哲學(xué)"
不要為了RESTful而RESTful
在能表達(dá)清楚的情況下,簡單就是美
接口路徑設(shè)計(jì)
接口設(shè)計(jì)原則
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
有一個(gè)唯一且私有的Profile
資源,只能通過User
操作Profile
。
更新user_id為123456的Profile資源 PUT http://~/$version/users/123456/profiles Request Body:{ "full_name": "cloud.mario", "state": "Beijing", "title": "一個(gè)開發(fā)者" }
Http Methods
HTTP Operation | Description |
---|
GET | 獲取,查找 |
POST | 新增創(chuàng)建 |
PUT | 更新 |
PATCH | 部分更新 |
DELETE | 刪除 |
URL組成
網(wǎng)絡(luò)協(xié)議(HTTP, HTTPS)
服務(wù)器地址
版本
接口名稱
?參數(shù)列表
GET https://github.com/v1/trades
為什么需要版本?
當(dāng)服務(wù)被更多其他系統(tǒng)使用的時(shí)候,服務(wù)的可用性和上下兼容變得至關(guān)重要。被外部系統(tǒng)依賴的服務(wù)在升級時(shí)是一個(gè)非常麻煩的事情,既要發(fā)布新的接口,又要保留舊的接口留出時(shí)間讓調(diào)用者去升級。在URL中加入Version標(biāo)示能很好地解決上下兼容(新老版本共存)問題。
示例1: URL中新增了Path parameter
v1版本
GET http://~/v1/trades?user_id=123456
v2版本
GET http://~/v2/:user_id/trades
示例1中的user_id
參數(shù)在v2版本被加入到path parameter中,使用$version
保證了v1
和v2
接口的共存。
示例2: 數(shù)據(jù)接口發(fā)生變化
v1版本
GET http://~/v1/accounts/cloud.mario Response Body: { "user_name": "cloud.mario", "e_mail": "cloud.mario@gmail.com", "state": "Beijign", "title": "一個(gè)開發(fā)者" }
v2版本
GET http://~/v2/accounts/cloud.mario Response Body: { "user_name": "cloud.mario", "e_mail": "cloud.mario@gmail.com", "profile": { "state": "Beijign", "title": "一個(gè)開發(fā)者" } }
示例2中的接口返回?cái)?shù)據(jù)結(jié)構(gòu)已經(jīng)發(fā)生了變化。使用$version
保證了v1
和v2
接口的共存。
URL定義限制
不使用大寫字母
使用中線-代替下劃線_
參數(shù)列表應(yīng)該被encode過
接口分類
資源對象的CURD操作
GET http://~/$version/trades #獲取trades列表 GET http://~/$version/trades/:id #根據(jù)id獲取單個(gè)trade POST http://~/$version/trades #創(chuàng)建trade PUT http://~/$version/trades/:id #根據(jù)id更新trade PATCH http://~/$version/trades/:id #根據(jù)id部分更新trade DELETE http://~/$version/trades/:id #根據(jù)id刪除trade
系統(tǒng)設(shè)置
使用settings
標(biāo)識,根據(jù)服務(wù)的屬性選擇http方法。
http://~/settings/$version/server-name
示例1: 搜索
GET http://~/services/$version/search?q=filter&category=file
示例2: 任務(wù)隊(duì)列操作
PUT http://~/services/$version/queued/jobs 往任務(wù)隊(duì)列里面添加一個(gè)新的任務(wù) DELETE http://~/services/$version/queued/jobs/:id 根據(jù)id刪除任務(wù)
示例3: 更改界面語言環(huán)境
PUT http://~/settings/$version/gui/lang { "lang": "zh-CN"}
為什么需要區(qū)分?
Microservices
Microservices
是一個(gè)全新的概念,它主要的觀點(diǎn)是將一個(gè)大型的服務(wù)系統(tǒng)分解成多個(gè)微型系統(tǒng)。每個(gè)微型系統(tǒng)都能獨(dú)立工作,并且提供各種不同的服務(wù)。獨(dú)立運(yùn)行的特點(diǎn)使微型系統(tǒng)之間不會產(chǎn)生相互影響,其中的一個(gè)微型系統(tǒng)宕機(jī)并不會牽連到其他的微型系統(tǒng)。這種架構(gòu)使分布式系統(tǒng)的節(jié)點(diǎn)數(shù)量大大提升。因?yàn)镽ESTful服務(wù)是無狀態(tài)的,所以這種分解并不會帶來狀態(tài)共享的問題。
路由規(guī)則(邏輯)
當(dāng)我們需要對不同屬性的接口做路由規(guī)則的時(shí)候,按功能劃分接口是一個(gè)很好的方案。例如:我們要對系統(tǒng)設(shè)置接口設(shè)置增加更嚴(yán)格的調(diào)用限制。
緩存
網(wǎng)絡(luò)接口相對于堆棧接口來說數(shù)據(jù)傳輸極其不穩(wěn)定,盡可能地減少數(shù)據(jù)傳輸不僅能控制這種風(fēng)險(xiǎn)還能減少流量。使用緩存還能有效地提高后臺的吞吐量。
后臺在響應(yīng)請求時(shí)使用響應(yīng)頭E-Tag
或Last-Modified
來標(biāo)記數(shù)據(jù)的版本,前臺在發(fā)送請求時(shí)將數(shù)據(jù)版本通過請求頭If-Match
幫助后臺判斷緩存的使用。
Request Header
If-Match: 2390239059405940
Response Header
E-Tag: 2390239059405940Last-Modified: 1403183502701
Bookmarker
在實(shí)際的環(huán)境中,有大量的查詢需求是相同的。將這些搜索需求標(biāo)簽化能降低使用難度也可以達(dá)到重用的目的。
示例: 查找狀態(tài)為關(guān)閉的訂單
普通方式
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的方式來描述程序的狀態(tài)信息
Link 主要包含以下屬性:
Property | Description |
---|
rel | 關(guān)聯(lián)內(nèi)容 |
href | URL |
type | 媒體類型 |
method | Http Method |
title | 標(biāo)題 |
arguments | 參數(shù)列表 |
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: 用戶注冊業(yè)務(wù)
用戶填寫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": "一個(gè)開發(fā)者" }
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: 請看下節(jié)<分頁>
HATEOAS在解決什么問題?
HATEOAS是Hypermedia as the Engine of Application State的縮寫形式,中文意思為:超媒體應(yīng)用狀態(tài)引擎。它的核心思想是使用超媒體表達(dá)應(yīng)用狀態(tài),與hypertext-driven思想是一致的。在此之前,我們大多數(shù)的程序業(yè)務(wù)控制在前臺完成。例如:我們會在前臺做注冊流程,我們在前臺判定下一步應(yīng)該做什么,可以做什么。當(dāng)使用HATEOAS時(shí),這些狀態(tài)流程控制都在應(yīng)用程序的后臺完成。我們使用超媒體來表達(dá)前臺做完某一步驟之后可以做哪些? 這樣一來,前臺的任務(wù)就變得相當(dāng)簡單了,前臺需要處理的是理解狀態(tài)表述,數(shù)據(jù)收集和結(jié)果顯示。
思考
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" } ] }
安全
調(diào)用限制
為保證服務(wù)的可用性應(yīng)對服務(wù)進(jìn)行調(diào)用過載保護(hù)
Response Headers
X-RateLimit-Limit: 3000 調(diào)用量的最大限制 X-RateLimit-Reset: 1403162176516 調(diào)用限制重置時(shí)間 X-RateLimit-Remaining: 299 剩余的調(diào)用量
安全驗(yàn)證
RESTful服務(wù)使用Oauth2的方式進(jìn)行調(diào)用授權(quán),使用http請求頭Authorization
設(shè)置授權(quán)碼; 必須使用User-Agent
設(shè)置客戶端信息, 無User-Agent
請求頭的請求應(yīng)該被拒絕訪問。
Request Header
User-Agent: Data-Server-Client Authorzation: Bearer 383w9JKJLJFw4ewpie2wefmjdlJLDJF
為什么建議使用Oauth2授權(quán)?
Oauth2的參與者為:客戶端,資源所有者,授權(quán)服務(wù)器,資源服務(wù)器。客戶端先從資源所有者得到授權(quán)碼之后使用授權(quán)碼從授權(quán)服務(wù)器得到token
,再使用token
調(diào)用資源服務(wù)器獲取經(jīng)過資源所有者授權(quán)使用的資源。這種授權(quán)方式的特點(diǎn)有:
資源所有者可以隨時(shí)撤銷授權(quán)許可
可以通過撤銷token
拒絕客戶端的調(diào)用
資源服務(wù)器可以拒絕客戶端的調(diào)用
通過這三種方式可以做到對資源的嚴(yán)格保護(hù)。資源的訪問權(quán)限也把握在資源所有者的手中,而不是資源服務(wù)器。
當(dāng)然,Oauth2
授權(quán)框架也允許受信任的客戶端直接使用token調(diào)用資源服務(wù)器獲取資源。這種靈活性完全取決于客戶端類型和對資源的保護(hù)程度。
為什么授權(quán)碼要放在Http Header中?
WEB服務(wù)器對訪問做記錄已經(jīng)成為了行業(yè)的一個(gè)標(biāo)準(zhǔn),訪問記錄不僅可以用來做訪問量統(tǒng)計(jì)還能用來做訪問特征分析?;ヂ?lián)網(wǎng)廣告平臺就是利用訪問記錄來做精準(zhǔn)營銷的。如果token
(授權(quán)碼)包含在URL中就有很大的安全風(fēng)險(xiǎn)。
包含在URL中的token
串可能被進(jìn)行重定向傳遞。通過這兩種方式入侵者可以不通過授權(quán)而使用泄漏的授權(quán)碼訪問那些受保護(hù)的數(shù)據(jù),會造成數(shù)據(jù)泄漏的風(fēng)險(xiǎn)。
以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
信息。
數(shù)據(jù)設(shè)計(jì)
交互原則
查詢,過濾條件使用query string。
用來描述數(shù)據(jù)或者請求的元數(shù)據(jù)放Header中,例如 X-Result-Fields
。
Content body 僅僅用來傳輸數(shù)據(jù)。
數(shù)據(jù)要做到拿來就可用的原則,不需要“拆箱”的過程。
結(jié)構(gòu)
使用JSON格式傳輸數(shù)據(jù),在http請求頭和響應(yīng)頭申明Content-Type
。返回的數(shù)據(jù)結(jié)構(gòu)應(yīng)該做到盡可能簡單,不要過于包裝。響應(yīng)狀態(tài)應(yīng)該包含在響應(yīng)頭中!
Request
Accept: application/json Content-Type: application/json;charset=UTF-8
Response
Content-Type: application/json;charset=UTF-8
錯(cuò)誤的做法
{ "status": 200, "data": { "trade_id": 1234, "trade_name": "Bala bala" } }
正確的做法
Response Headers: Status: 200 Response Body: { "trade_id": 1234, "trade_name": "Bala bala" }
示例1: 創(chuàng)建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 是一種可以跨平臺高擴(kuò)展的輕量級的數(shù)據(jù)交換格式。易于人閱讀和編寫,同時(shí)也易于機(jī)器解析和生成。
屬性定義限制
不能使用大寫(大小寫友好)
使用下劃線_命名(連接兩個(gè)單詞)
屬性和字符串值必須使用雙引號""
提取部分字段
無狀態(tài)服務(wù)器應(yīng)該允許客戶端對數(shù)據(jù)按需提取。在請求頭使用X-Result-Fields
指定數(shù)據(jù)返回的字段集合。
例如:trade 有trade_id,
trade_name
, created_at
三個(gè)屬性,客戶端只需其中的trade_id
與trade_name
屬性。
Request Header
X-Result-Fields: trade_id,trade_name
子對象描述
數(shù)據(jù)里面的子對象使用URI描述不應(yīng)該被提取,除非用戶指定需要提取子對象
示例: trade
里面的order
對象
錯(cuò)誤的做法
{ "trade_id": "123456789", "full_path": null, "order": { "order_id": "987654321" } }
正確的做法
{ "trade_id": "123456789", "order": "http://~/$version/orders/987654321" }
應(yīng)用指定提取子對象,需要在請求頭聲明X-Expansion-Fields
Request
X-Expansion-Fields: true
為什么要客戶端指定提取子對象時(shí)才提取?
懶模式服務(wù)能夠最大程度地節(jié)省運(yùn)算資源。雖然與客戶端交互的次數(shù)有所增加,但是能做到按需提取,按需響應(yīng),這也是響應(yīng)式設(shè)計(jì)的一大特點(diǎn)??蛻舳说挠脩粜袨槟J綗o法真實(shí)地模擬,也就無法確定哪些資源需要做到一次性推送,讓客戶端按需使用是一個(gè)不錯(cuò)的方式。
關(guān)于空字段
應(yīng)該在返回結(jié)果里面剔除空字段,因?yàn)閚ull值傳輸?shù)娇蛻舳瞬]有實(shí)際的含義,反而增加了占用空間。
Tips
使用HTTP Header時(shí),優(yōu)先使用合適的標(biāo)準(zhǔn)頭屬性。用X-
作為前綴自定義一個(gè)頭屬性,例如: X-Result-Fields
。
狀態(tài)碼&錯(cuò)誤處理
應(yīng)用狀態(tài)碼
Code | HTTP Operation | Body Contents | Description |
---|
200 | GET,PUT | 資源 | 操作成功 |
201 | POST | 資源,元數(shù)據(jù) | 對象創(chuàng)建成功 |
202 | POST,PUT,DELETE,PATCH | N/A | 請求已經(jīng)被接受 |
204 | DELETE,PUT,PATCH | N/A | 操作已經(jīng)執(zhí)行成功,但是沒有返回?cái)?shù)據(jù) |
301 | GET | link | 資源已被移除 |
303 | GET | link | 重定向 |
304 | GET | N/A | 資源沒有被修改 |
400 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 參數(shù)列表錯(cuò)誤(缺少,格式不匹配) |
401 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 未授權(quán) |
403 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 訪問受限,授權(quán)過期 |
404 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 資源,服務(wù)未找到 |
405 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 不允許的http方法 |
409 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 資源沖突,或者資源被鎖定 |
415 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 不支持的數(shù)據(jù)(媒體)類型 |
429 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 請求過多被限制 |
500 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 系統(tǒng)內(nèi)部錯(cuò)誤 |
501 | GET,PSOT,PUT,DELETE,PATCH | 錯(cuò)誤提示(消息) | 接口未實(shí)現(xiàn) |
容器狀態(tài)碼
容器狀態(tài)碼是指http容器的狀態(tài)碼,應(yīng)用不應(yīng)該使用或限制使用
Code | HTTP Operation | Body Contents | Description |
---|
303 | GET | link | 靜態(tài)資源被移除,應(yīng)用限制使用 |
503 | GET,PSOT,PUT,DELETE,PATCH | text body | 服務(wù)器宕機(jī) |
Tips
4開頭的錯(cuò)誤用來表達(dá)來自于客戶端的錯(cuò)誤,例如: 未授權(quán),參數(shù)缺失。5開頭的錯(cuò)誤用來表達(dá)服務(wù)端的錯(cuò)誤,例如: 在連接外部系統(tǒng)(DB)發(fā)生的IO錯(cuò)誤。
錯(cuò)誤信息格式
錯(cuò)誤信息應(yīng)該包含下列內(nèi)容:
錯(cuò)誤標(biāo)題 message
, 必須
錯(cuò)誤代碼 error code
, 必須
錯(cuò)誤信息 error message
, 必須
資源 resource
, 可選
屬性 field
, 可選
文檔地址 document
, 可選
Tips
Error Code
盡可能做到簡潔明了,提取異常的關(guān)鍵字并且使用下劃線_把它們連接起來。
示例: 調(diào)用頻率超過限制,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數(shù)據(jù)(返回結(jié)果)并且使用gzip壓縮,Pettyprint易于閱讀,多余的空格在經(jīng)過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
資源福利
框架&工具
參考資料