撰寫合格的REST API

2015-06-28    分類:WEB開發、程式設計開發、首頁精華3人評論發表於2015-06-28

本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

兩週前因為公司一次裁人,好幾個人的活都被按在了我頭上,這其中的一大部分是一系列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 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://www.codeceo.com/article/rest-api.html
翻譯作者:碼農網 – 小峰
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章