撰寫合格的REST API

兩週前因爲公司一次裁人,好幾個人的活都被按在了我頭上,這其中的一大部分是一系列REST API,撰寫者號稱基本完成,我測試了一下,發現儘管從功能的角度來說,這些API實現了spec的顯式要求,但是從實際使用的角度,欠缺的東西太多(各種各樣的隱式需求)。REST API是一個系統的backend和frontend(或者3rd party)打交道的通道,承前啓後,有很多很多隱式需求,比如調用接口與RFC保持一致,API的內在和外在的安全性等等,並非提供幾個endpoint,返回相應的json數據那麼簡單。仔細研究了原作者的代碼,發現缺失的東西實在太多,每個API基本都在各自爲戰,與其修補,不如重寫(並非是程序員相輕的緣故),於是我花了一整週,重寫了所有的API。稍稍總結了些經驗,在這篇文章裏講講如何撰寫「合格的」REST API。

RFC一致性

REST API一般用來將某種資源和允許的對資源的操作暴露給外界,使調用者能夠以正確的方式操作資源。這裏,在輸入輸出的處理上,要符合HTTP/1.1(不久的將來,要符合HTTP/2.0)的RFC,保證接口的一致性。這裏主要講輸入的method/headers和輸出的status code。

Methods

HTTP協議提供了很多methods來操作數據:

  • GET: 獲取某個資源,GET操作應該是冪等(idempotence)的,且無副作用。

  • POST: 創建一個新的資源。

  • PUT: 替換某個已有的資源。PUT操作雖然有副作用,但其應該是冪等的。

  • PATCH(RFC5789): 修改某個已有的資源。

  • 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 API時,

簡單說來就是一個操作符合冪等性,那麼相同的數據和參數下,執行一次或多次產生的效果(副作用)是一樣的。

現在大多的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=


(Amazon HMAC圖示)

在做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相關的部分。

轉載:http://mp.weixin.qq.com/s?__biz=MzA3NDM0ODQwMw==&mid=208060670&idx=1&sn=ce67b8896985e8448137052b338093e0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章