技術(shù)熱點(diǎn):RESTful API 最佳實(shí)踐
在參考了GitHub API設(shè)計(jì)和大量博客文章后總結(jié)了一下RESTful API的設(shè)計(jì),分享如下。想要更好的理解RESTful API首先需要理解如下概念:
REST:REST(Representational State Transfer)這個(gè)詞,是Roy Thomas Fielding在他2000年的博士論文中提出的,翻譯成中文大意為表現(xiàn)層狀態(tài)傳輸。由于他是HTTP協(xié)議(1.0版和1.1版)的主要設(shè)計(jì)者、Apache服務(wù)器軟件的作者之一、Apache基金會(huì)的***任主席,所以REST原則迅速流行起來。當(dāng)一個(gè)軟件架構(gòu)符合REST原則,我們稱之為RESTful架構(gòu)。說了這么多,我們?yōu)槭裁匆褂肦ESTful架構(gòu)?使用RESTful架構(gòu)有什么好處?因?yàn)榘凑誖ESTful架構(gòu)可以充分的利用HTTP協(xié)議帶給我們的各種功能,算是對(duì)HTTP協(xié)議使用的***實(shí)踐,還有一點(diǎn)就是可以使軟件架構(gòu)設(shè)計(jì)更加清晰,可維護(hù)性更好,但是并不是所有情況都需要完全遵守REST原則,畢竟實(shí)際情況遠(yuǎn)遠(yuǎn)比REST原則所定義的更加復(fù)雜,下面會(huì)詳細(xì)介紹。
冪等性:冪等性(Idempotence)本身是一個(gè)數(shù)學(xué)概念,在HTTP/1.1規(guī)范中冪等性的定義是:
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.
翻譯過來大意就是如果方法調(diào)用一次和多次產(chǎn)生額外的效果是相同的,它就具有冪等性。
例子:在HTTP中使用GET方法通常用于從服務(wù)器獲取資源,無論調(diào)用多少次產(chǎn)生的額外效果都是從服務(wù)器獲取資源,所以GET具有冪等性;而POST方法通常用于提交數(shù)據(jù)在服務(wù)器上創(chuàng)建一個(gè)資源,由于最終創(chuàng)建的結(jié)果每次都是不同的,所以POST不具有冪等性;但是PUT方法卻是冪等的,因?yàn)槊看握{(diào)用產(chǎn)生的效果都是對(duì)資源進(jìn)行更新。
安全方法:安全方法是指不修改資源的 HTTP 方法。譬如,當(dāng)使用 GET 或者 HEAD 作為資源 URL,都必須不去改變資源。然而,這并不全準(zhǔn)確。意思是:它不改變資源的 表示形式。對(duì)于安全方法,它仍然可能改變服務(wù)器上的內(nèi)容或資源,但這必須不導(dǎo)致不同的表現(xiàn)形式。
有關(guān)HTTP常用方法冪等性和安全性如下:
RESTful API設(shè)計(jì)規(guī)則:
1. URI
- 應(yīng)該將API部署在專用域名之下:https://api.example.com
- 不用大寫
- 用中杠-不用下杠_;
- 參數(shù)列表要encode;
- URI中不應(yīng)該出現(xiàn)動(dòng)詞,動(dòng)詞應(yīng)該使用HTTP方法表示,但是如果無法表示,也可使用動(dòng)詞,例如:search沒有對(duì)應(yīng)的HTTP方法,可以在路徑中使用search,更加直觀;
- URI中的名詞表示資源集合,使用復(fù)數(shù)形式;
- 雖然/在URI中表達(dá)層級(jí),但是避免為了追求REST導(dǎo)致層級(jí)過深,適當(dāng)使用參數(shù)表示。
GET /comments/tid/tid=1&page=1
2. Request:通過標(biāo)準(zhǔn)HTTP方法對(duì)資源CRUD
- GET:查詢資源
GET /comments //獲取所有評(píng)論 GET /comments/tid/1 //獲取文章tid為1的所有評(píng)論
- POST:創(chuàng)建資源
POST /comments/tid/1 //為tid為1的文章創(chuàng)建評(píng)論
- PUT:更新資源
PUT /comments/cid/like/1 //為cid為1的評(píng)論點(diǎn)贊
- DELETE:刪除資源
DELETE /comments/cid/1 //刪除cid為1的評(píng)論
3. Response
- 采用JSON,不要使用XML
- 默認(rèn)情況下JSON外層不需要嵌套大括號(hào),API需要支持JSONP跨域訪問或者客戶端無法訪問HTTP Header才需要加上嵌套大括號(hào)
- 默認(rèn)情況下不要過濾API輸出中的空格,并且要支持gzip
4. API版本控制
- 在URI中存放:GET /v1/comments;
- 客戶端在Accept Header中存放:Accept: application/vnd.github.v3+json,服務(wù)器自定義Header返回當(dāng)前版本信息:X-GitHub-Media-Type: github.v3; format=json(GitHub在用);
- 以上兩種方法根據(jù)情況選擇,Github用的方式是REST中所要求的方式;
- 測(cè)試API和正式API要進(jìn)行區(qū)分,方式通過如上兩種方式實(shí)現(xiàn)。
5. 速度限制
為了避免請(qǐng)求泛濫,給API設(shè)置速度限制很重要。為此 RFC 6585 引入了HTTP狀態(tài)碼429(too many requests)。加入速度設(shè)置之后,應(yīng)該提示用戶,至于如何提示標(biāo)準(zhǔn)上沒有說明,不過流行的方法是使用HTTP的返回頭。
下面是幾個(gè)必須的返回頭(依照twitter的命名規(guī)則):
- X-Rate-Limit-Limit :當(dāng)前時(shí)間段允許的并發(fā)請(qǐng)求數(shù)
- X-Rate-Limit-Remaining:當(dāng)前時(shí)間段保留的請(qǐng)求數(shù)。
- X-Rate-Limit-Reset:當(dāng)前時(shí)間段剩余秒數(shù)
為什么使用當(dāng)前時(shí)間段剩余秒數(shù)而不是時(shí)間戳?
時(shí)間戳保存的信息很多,但是也包含了很多不必要的信息,用戶只需要知道還剩幾秒就可以再發(fā)請(qǐng)求了這樣也避免了clock skew問題。
6.緩存
HTTP提供了自帶的緩存框架。你需要做的是在返回的時(shí)候加入一些返回頭信息,在接受輸入的時(shí)候加入輸入驗(yàn)證?;緝煞N方法:
- ETag:當(dāng)生成請(qǐng)求的時(shí)候,在HTTP頭里面加入ETag,其中包含請(qǐng)求的校驗(yàn)和和哈希值,這個(gè)值和在輸入變化的時(shí)候也應(yīng)該變化。如果輸入的HTTP請(qǐng)求包含IF-NONE-MATCH頭以及一個(gè)ETag值,那么API應(yīng)該返回304 not modified狀態(tài)碼,而不是常規(guī)的輸出結(jié)果。
- Last-Modified:和etag一樣,只是多了一個(gè)時(shí)間戳。返回頭里的Last-Modified:包含了 RFC 1123 時(shí)間戳,它和IF-MODIFIED-SINCE一致。HTTP規(guī)范里面有三種date格式,服務(wù)器應(yīng)該都能處理。
7.覆蓋HTTP方法
一些HTTP客戶端只支持GET和POST請(qǐng)求。為了能夠加強(qiáng)這些客戶端的訪問能力,API需要能夠覆蓋HTTP方法。盡管這里沒有任何強(qiáng)制的標(biāo)準(zhǔn),但流行的做法是API會(huì)接收一個(gè)請(qǐng)求頭X-HTTP-Method-Override,它的值可以是PUT、PATCH或者DELETE三者之一。
注意,用來覆蓋HTTP方法的header只能在POST請(qǐng)求中被接受。GET請(qǐng)求永遠(yuǎn)不能修改服務(wù)器上的數(shù)據(jù)。
8.過濾信息
如果記錄數(shù)量很多,服務(wù)器不可能都將它們返回給用戶。API應(yīng)該提供參數(shù),過濾返回結(jié)果。
下面是一些常見的參數(shù):
?limit=10:指定返回記錄的數(shù)量
?offset=10:指定返回記錄的開始位置。
?page=2&per_page=100:指定第幾頁,以及每頁的記錄數(shù)。
?sortby=name&order=asc:指定返回結(jié)果按照哪個(gè)屬性排序,以及排序順序。
?animal_type_id=1:指定篩選條件
就像HTML的出錯(cuò)頁面向訪問者展示了有用的錯(cuò)誤消息一樣,API也應(yīng)該用之前熟悉易讀的格式來提供有用的錯(cuò)誤消息。錯(cuò)誤的表現(xiàn)形式應(yīng)該跟其他資源保持一致,只是用一些自己的字段。
API應(yīng)該一直返回合理的HTTP狀態(tài)碼。API錯(cuò)誤一般情況下分成兩類:代表客戶端錯(cuò)誤的400系列狀態(tài)碼和代表服務(wù)端錯(cuò)誤的500系列狀態(tài)碼。API至少把所有400系列錯(cuò)誤統(tǒng)一用易讀的JSON格式來展示。如果可能(比如,如果負(fù)載均衡和反向代理能夠創(chuàng)建自定義錯(cuò)誤內(nèi)容的話),500系列的狀態(tài)碼也這么弄。
JSON錯(cuò)誤內(nèi)容應(yīng)該為開發(fā)者提供一些東西 – 有用的錯(cuò)誤消息,唯一的錯(cuò)誤碼(通過它可以在文檔中找到更多錯(cuò)誤細(xì)節(jié)),可能的話提供錯(cuò)誤細(xì)節(jié)描述。用JSON格式來輸出錯(cuò)誤看起來這樣:
{
"code" : 1234,
"message" : "Something bad happened : (",
"description" : "More details about the error here"
}
對(duì)于PUT、PATCH和POST的請(qǐng)求進(jìn)行的校驗(yàn)錯(cuò)誤需要嵌套多個(gè)字段。***做法是用固定的錯(cuò)誤碼來表示校驗(yàn)失敗,然后在額外的errors字段中提供錯(cuò)誤的細(xì)節(jié),像這樣:
{
"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狀態(tài)碼
HTTP定義了很多有意義的狀態(tài)碼,你可以在你的API中使用。這些狀態(tài)碼可以幫助API消費(fèi)者用來路由它們獲取到的響應(yīng)內(nèi)容。整理了一個(gè)你肯定會(huì)用到的狀態(tài)碼列表:
- 200 OK – 對(duì)成功的GET、PUT、PATCH或DELETE操作進(jìn)行響應(yīng)。也可以被用在不創(chuàng)建新資源的POST操作上
- 201 Created – 對(duì)創(chuàng)建新資源的POST操作進(jìn)行響應(yīng)。應(yīng)該帶著指向新資源地址的Location header)
- 204 No Content – 對(duì)不會(huì)返回響應(yīng)體的成功請(qǐng)求進(jìn)行響應(yīng)(比如DELETE請(qǐng)求)
- 304 Not Modified – HTTP緩存header生效的時(shí)候用
- 400 Bad Request – 請(qǐng)求異常,比如請(qǐng)求中的body無法解析
- 401 Unauthorized – 沒有進(jìn)行認(rèn)證或者認(rèn)證非法。當(dāng)API通過瀏覽器訪問的時(shí)候,可以用來彈出一個(gè)認(rèn)證對(duì)話框
- 403 Forbidden – 當(dāng)認(rèn)證成功,但是認(rèn)證過的用戶沒有訪問資源的權(quán)限
- 404 Not Found – 當(dāng)一個(gè)不存在的資源被請(qǐng)求
- 405 Method Not Allowed – 所請(qǐng)求的HTTP方法不允許當(dāng)前認(rèn)證用戶訪問
- 410 Gone – 表示當(dāng)前請(qǐng)求的資源不再可用。當(dāng)調(diào)用老版本API的時(shí)候很有用
- 415 Unsupported Media Type – 如果請(qǐng)求中的內(nèi)容類型是錯(cuò)誤的
- 422 Unprocessable Entity – 用來表示校驗(yàn)錯(cuò)誤
- 429 Too Many Requests – 由于請(qǐng)求頻次達(dá)到上限而被拒絕訪問
11.認(rèn)證
RESTful API應(yīng)該是無狀態(tài)。這意味著對(duì)請(qǐng)求的認(rèn)證不應(yīng)該基于cookie或者session。相反,每個(gè)請(qǐng)求應(yīng)該帶有一些認(rèn)證憑證。
如果一直使用SSL,認(rèn)證憑證可以簡單的使用隨機(jī)生成的access token,把其做為HTTP Basic Auth中user name字段的值傳給API。這么做的好處是可以通過瀏覽器訪問 – 如果瀏覽器從服務(wù)器收到401 Unauthorized狀態(tài)碼,它將會(huì)彈出一個(gè)對(duì)話框讓人輸出認(rèn)證憑證。
當(dāng)然,這種基于token來進(jìn)行基本認(rèn)證的方法只能當(dāng)用戶從API管理后臺(tái)拷貝了一個(gè)token到自己的代碼中才行。如果搞不到token,只能使用OAuth 2來把安全token傳遞給第三方。OAuth 2使用Bearer token,并且也是基于SSL來保證傳輸安全。
支持JSONP的API可能需要第三種方法來實(shí)現(xiàn)認(rèn)證,因?yàn)镴SONP的請(qǐng)求沒法發(fā)送HTTP Basic Auth憑證或者Bearer token。這種情況下,可以使用一個(gè)額外的查詢參數(shù)access_token。注意:使用查詢參數(shù)來傳遞token存在一個(gè)固有的安全隱患,因?yàn)榇蠖鄶?shù)web服務(wù)器會(huì)在服務(wù)器日志中保存查詢參數(shù)。
不管怎么樣,以上三種方法是用來在API之間傳輸token的方法。實(shí)際傳輸?shù)膖oken可以是一樣的。
12.使用SSL
一定要使用SSL。沒有例外。如今,你的web API可以從任何有互聯(lián)網(wǎng)的地方(像圖書館,咖啡館,機(jī)場(chǎng)等等)被訪問到。這些地方并不都是安全的。很多地方根本沒有對(duì)網(wǎng)絡(luò)連接進(jìn)行加密,如果認(rèn)證憑證被劫持的話,這樣訪問者很容易被竊聽或者被冒充。
一直使用SSL的另一個(gè)優(yōu)勢(shì)是,加密的連接簡化了用戶認(rèn)證的工作 – 你可以使用簡單的access token,而不需要對(duì)每個(gè)API請(qǐng)求進(jìn)行簽名。
需要注意的一件事是以非SSL的形式訪問API的URL。不要把請(qǐng)求跳轉(zhuǎn)到它們的SSL版本上。直接拋出一個(gè)嚴(yán)重錯(cuò)誤!
13.Hypermedia API
RESTful API***做到Hypermedia,即返回結(jié)果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應(yīng)該做什么。
比如,當(dāng)用戶向http://api.example.com的根目錄發(fā)出請(qǐng)求,會(huì)得到這樣一個(gè)文檔。
{"link": {
"rel": "collection https://www.example.com/comments",
"href": "https://api.example.com/comments",
"title": "List of comments",
"type": "application/vnd.yourformat+json"
}}
上面代碼表示,文檔中有一個(gè)link屬性,用戶讀取這個(gè)屬性就知道下一步該調(diào)用什么API了。rel表示這個(gè)API與當(dāng)前網(wǎng)址的關(guān)系(collection關(guān)系,并給出該collection的網(wǎng)址),href表示API的路徑,title表示API的標(biāo)題,type表示返回類型。
Hypermedia API的設(shè)計(jì)被稱為HATEOAS。
在進(jìn)行分頁查詢時(shí)可以返回下一頁的URI,如果沒有說明服務(wù)器已經(jīng)取到***一條數(shù)據(jù)了,客戶端可以減少不必要的請(qǐng)求以及URI的構(gòu)造,建議在分頁的情況下使用。
這就是我總結(jié)出來的RESTful API的***設(shè)計(jì)實(shí)踐,文章如果有紕漏,歡迎指出。