兩週前由於公司一次裁員,好幾我的的活都被按在了我頭上,這其中的一大部分是一系列REST API,撰寫者號稱基本完成,我測試了一下,發現儘管從功能的角度來講,這些API實現了spec的顯式要求,可是從實際使用的角度,欠缺的東西太多(各類各樣的隱式需求)。REST API是一個系統的backend和frontend(或者3rd party)打交道的通道,承前啓後,有不少不少隱式需求,好比調用接口與RFC保持一致,API的內在和外在的安全性等等,並不是提供幾個endpoint,返回相應的json數據那麼簡單。仔細研究了原做者的代碼,發現缺失的東西實在太多,每一個API基本都在各自爲戰,與其修補,不如重寫(並不是是程序員相輕的緣故),因而我花了一整週,重寫了全部的API。稍稍總結了些經驗,在這篇文章裏講講如何撰寫「合格的」REST API。python
RFC一致性git
REST API通常用來將某種資源和容許的對資源的操做暴露給外界,使調用者可以以正確的方式操做資源。這裏,在輸入輸出的處理上,要符合HTTP/1.1(不久的未來,要符合HTTP/2.0)的RFC,保證接口的一致性。這裏主要講輸入的method/headers和輸出的status code。程序員
Methodsgithub
HTTP協議提供了不少methods來操做數據:web
GET: 獲取某個資源,GET操做應該是冪等(idempotence)的,且無反作用。數據庫
POST: 建立一個新的資源。django
PUT: 替換某個已有的資源。PUT操做雖然有反作用,但其應該是冪等的。json
PATCH(RFC5789): 修改某個已有的資源。flask
DELETE:刪除某個資源。DELETE操做有反作用,但也是冪等的。緩存
冪等在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.
簡單說來就是一個操做符合冪等性,那麼相同的數據和參數下,執行一次或屢次產生的效果(反作用)是同樣的。
如今大多的REST framwork對HTTP methods都有正確的支持,有些舊的framework可能未必對PATCH有支持,須要注意。若是本身手寫REST API,必定要注意區分POST/PUT/PATCH/DELETE的應用場景。
Headers
不少REST API犯的比較大的一個問題是:不怎麼理會request headers。對於REST API,有一些HTTP headers很重要:
Accept:服務器須要返回什麼樣的content。若是客戶端要求返回"application/xml",而服務器端只能返回"application/json",那麼最好返回status code 406 not acceptable(RFC2616),固然,返回application/json也並不違背RFC的定義。一個合格的REST API須要根據Accept頭來靈活返回合適的數據。
If-Modified-Since/If-None-Match:若是客戶端提供某個條件,那麼當這條件知足時,才返回數據,不然返回304 not modified。好比客戶端已經緩存了某個數據,它只是想看看有沒有新的數據時,會用這兩個header之一,服務器若是不理不睬,依舊作足全套功課,返回200 ok,那就既不專業,也不高效了。
If-Match:在對某個資源作PUT/PATCH/DELETE操做時,服務器應該要求客戶端提供If-Match頭,只有客戶端提供的Etag與服務器對應資源的Etag一致,才進行操做,不然返回412 precondition failed。這個頭很是重要,下文詳解。
Status Code
不少REST API犯下的另外一個錯誤是:返回數據時不遵循RFC定義的status code,而是一概200 ok + error message。這麼作在client + API都是同一公司所爲還湊合可用,但一旦把API暴露給第三方,不但貽笑大方,還會留下諸多互操做上的隱患。
以上僅僅是最基本的一些考慮,要作到徹底符合RFC,除了參考RFC自己之外,erlang社區的webmachine或者clojure下的liberator都是不錯的實現,是目前爲數很少的REST API done right的library/framework。
(liberator的decision tree,沿襲了webmachine的思想,請自行google其文檔查看大圖)
安全性
前面說過,REST API承前啓後,是系統暴露給外界的接口,因此,其安全性很是重要。安全並單單不意味着加密解密,而是一致性(integrity),機密性(confidentiality)和可用性(availibility)。
請求數據驗證
咱們從數據流入REST API的第一步 —— 請求數據的驗證 —— 來保證安全性。你能夠把請求數據驗證當作一個巨大的漏斗,把沒必要要的訪問通通過濾在第一線:
Request headers是否合法:若是出現了某些不應有的頭,或者某些必須包含的頭沒有出現或者內容不合法,根據其錯誤類型一概返回4xx。好比說你的API須要某個特殊的私有頭(e.g. X-Request-ID),那麼凡是沒有這個頭的請求一概拒絕。這能夠防止各種漫無目的的webot或crawler的請求,節省服務器的開銷。
Request URI和Request body是否合法:若是請求帶有了不應有的數據,或者某些必須包含的數據沒有出現或內容不合法,一概返回4xx。好比說,API只容許querystring中含有query,那麼"?sort=desc"這樣的請求須要直接被拒絕。有很多攻擊會在querystring和request body裏作文章,最好的對應策略是,過濾全部含有不應出現的數據的請求。
數據完整性驗證
REST API每每須要對backend的數據進行修改。修改是個很可怕的操做,咱們既要保證正常的服務請求可以正確處理,還須要防止各類潛在的攻擊,如replay。數據完整性驗證的底線是:保證要修改的數據和服務器裏的數據是一致的 —— 這是經過Etag來完成。
Etag能夠認爲是某個資源的一個惟一的版本號。當客戶端請求某個資源時,該資源的Etag一同被返回,而當客戶端須要修改該資源時,須要經過"If-Match"頭來提供這個Etag。服務器檢查客戶端提供的Etag是否和服務器同一資源的Etag相同,若是相同,才進行修改,不然返回412 precondition failed。
使用Etag能夠防止錯誤更新。好比A拿到了Resource X的Etag X1,B也拿到了Resource X的Etag X1。B對X作了修改,修改後系統生成的新的Etag是X2。這時A也想更新X,因爲A持有舊的Etag,服務器拒絕更新,直至A從新獲取了X後才能正常更新。
Etag相似一把鎖,是數據完整性的最重要的一道保障。Etag能把絕大多數integrity的問題扼殺在搖籃中,固然,race condition仍是存在的:若是B的修改還未進入數據庫,而A的修改請求正好經過了Etag的驗證時,依然存在一致性問題。這就須要在數據庫寫入時作一致性寫入的前置檢查。
訪問控制
REST API須要清晰定義哪些操做可以公開訪問,哪些操做須要受權訪問。通常而言,若是對REST API的安全性要求比較高,那麼,全部的API的全部操做均需獲得受權。
在HTTP協議之上處理受權有不少方法,如HTTP BASIC Auth,OAuth,HMAC Auth等,其核心思想都是驗證某個請求是由一個合法的請求者發起。Basic Auth會把用戶的密碼暴露在網絡之中,並不是最安全的解決方案,OAuth的核心部分與HMAC Auth差很少,只不過多了不少與token分發相關的內容。這裏咱們主要講講HMAC Auth的思想。
回到Security的三個屬性:一致性,機密性,和可用性。HMAC Auth保證一致性:請求的數據在傳輸過程當中未被修改,所以能夠安全地用於驗證請求的合法性。
HMAC主要在請求頭中使用兩個字段:Authorization和Date(或X-Auth-Timestamp)。Authorization字段的內容由":"分隔成兩部分,":"前是access-key,":"後是HTTP請求的HMAC值。在API受權的時候通常會爲調用者生成access-key和access-secret,前者能夠暴露在網絡中,後者必須安全保存。當客戶端調用API時,用本身的access-secret按照要求對request的headers/body計算HMAC,而後把本身的access-key和HMAC填入Authorization頭中。服務器拿到這個頭,從數據庫(或者緩存)中取出access-key對應的secret,按照相同的方式計算HMAC,若是其與Authorization header中的一致,則請求是合法的,且未被修改過的;不然不合法。
GET /photos/puppy.jpg HTTP/1.1 Host: johnsmith.s3.amazonaws.com Date: Mon, 26 Mar 2007 19:37:58 +0000
Authorization: AWS AKIAIOSFODNN7EXAMPLE:frJIUN8DYpKDtOLCwo//yllqDzg=
在作HMAC的時候,request headers中的request method,request URI,Date/X-Auth-Timestamp等header會被計算在HMAC中。將時間戳計算在HMAC中的好處是能夠防止replay攻擊。客戶端和服務器之間的UTC時間正常來講誤差很小,那麼,一個請求攜帶的時間戳,和該請求到達服務器時服務器的時間戳,中間差異太大,超過某個閾值(好比說120s),那麼能夠認爲是replay,服務器主動丟棄該請求。
使用HMAC能夠很大程度上防止DOS攻擊 —— 無效的請求在驗證HMAC階段就被丟棄,最大程度保護服務器的計算資源。
HTTPS
HMAC Auth儘管在保證請求的一致性上很是安全,能夠用於鑑別請求是否由合法的請求者發起,但請求的數據和服務器返回的響應都是明文傳輸,對某些要求比較高的API來講,安全級別還不夠。這時候,須要部署HTTPS。在其之上再加一層屏障。
其餘
作到了接口一致性(符合RFC)和安全性,REST API能夠算得上是合格了。固然,一個實現良好的REST API還應該有以下功能:
rate limiting:訪問限制。
metrics:服務器應該收集每一個請求的訪問時間,到達時間,處理時間,latency,便於瞭解API的性能和客戶端的訪問分佈,以便更好地優化性能和應對突發請求。
docs:豐富的接口文檔 —— API的調用者須要詳盡的文檔來正確調用API,能夠用swagger來實現。
hooks/event propogation:其餘系統可以比較方便地與該API集成。好比說添加了某資源後,經過kafka或者rabbitMQ向外界暴露某個消息,相應的subscribers能夠進行必要的處理。不過要注意的是,hooks/event propogation可能會破壞REST API的冪等性,須要當心使用。
各個社區裏面比較成熟的REST API framework/library: Python: django-rest-framework(django),eve(flask)。各有千秋。惋惜python沒有好的相似webmachine的實現。
Erlang/Elixir: webmachine/ewebmachine。 Ruby: webmachine-ruby。 Clojure:liberator。
其它語言接觸很少,就不介紹了。能夠經過訪問該語言在github上相應的awesome repo(google awesome XXX,如awesome python),查看REST API相關的部分。