之前一篇文章寫過REST服務介紹, 今天再次來自回顧一下. REST是一種架構風格. 首次出現在2000年Roy Fielding的博士論文中,Roy Fielding是 HTTP 規範的主要編寫者之一。 論文中提到:“我這篇文章的寫作目的,就是想在符合架構原理的前提下,理解和評估以網路為基礎的應用...
之前一篇文章寫過REST服務介紹, 今天再次來自回顧一下. REST是一種架構風格. 首次出現在2000年Roy Fielding的博士論文中,Roy Fielding是 HTTP 規範的主要編寫者之一。 論文中提到:“我這篇文章的寫作目的,就是想在符合架構原理的前提下,理解和評估以網路為基礎的應用軟體的架構設計,得到一個功能強、性能好、適宜通信的架構。REST指的是一組架構約束條件和原則。” 如果一個架構符合REST的約束條件和原則,我們就稱它為RESTful架構。
RESTFul架構應該遵循統一介面原則,統一介面包含了一組受限的預定義的操作,不論什麼樣的資源,都是通過使用相同的介面進行資源的訪問。介面應該使用標準的HTTP方法如GET,PUT和POST,並遵循這些方法的語義。
如果按照HTTP方法的語義來暴露資源,那麼介面將會擁有安全性和冪等性的特性,例如GET和HEAD請求都是安全的, 無論請求多少次,都不會改變伺服器狀態。而GET、HEAD、PUT和DELETE請求都是冪等的,無論對資源操作多少次, 結果總是一樣的,後面的請求並不會產生比第一次更多的影響。
統一資源介面
下麵列出了GET,DELETE,PUT和POST的典型用法:
GET
- 安全且冪等
- 獲取表示
-
變更時獲取表示(緩存)
-
200(OK) - 表示已在響應中發出
- 204(無內容) - 資源有空表示
- 301(Moved Permanently) - 資源的URI已被更新
- 303(See Other) - 其他(如,負載均衡)
- 304(not modified)- 資源未更改(緩存)
- 400 (bad request)- 指代壞請求(如,參數錯誤)
- 404 (not found)- 資源不存在
- 406 (not acceptable)- 服務端不支持所需表示
- 500 (internal server error)- 通用錯誤響應
- 503 (Service Unavailable)- 服務端當前無法處理請求
POST
- 不安全且不冪等
- 使用服務端管理的(自動產生)的實例號創建資源
- 創建子資源
- 部分更新資源
-
如果沒有被修改,則不過更新資源(樂觀鎖)
-
200(OK)- 如果現有資源已被更改
- 201(created)- 如果新資源被創建
- 202(accepted)- 已接受處理請求但尚未完成(非同步處理)
- 301(Moved Permanently)- 資源的URI被更新
- 303(See Other)- 其他(如,負載均衡)
- 400(bad request)- 指代壞請求
- 404 (not found)- 資源不存在
- 406 (not acceptable)- 服務端不支持所需表示
- 409 (conflict)- 通用衝突
- 412 (Precondition Failed)- 前置條件失敗(如執行條件更新時的衝突)
- 415 (unsupported media type)- 接受到的表示不受支持
- 500 (internal server error)- 通用錯誤響應
- 503 (Service Unavailable)- 服務當前無法處理請求
PUT
- 不安全但冪等
- 用客戶端管理的實例號創建一個資源
- 通過替換的方式更新資源
-
如果未被修改,則更新資源(樂觀鎖)
-
200 (OK)- 如果已存在資源被更改
- 201 (created)- 如果新資源被創建
- 301(Moved Permanently)- 資源的URI已更改
- 303 (See Other)- 其他(如,負載均衡)
- 400 (bad request)- 指代壞請求
- 404 (not found)- 資源不存在
- 406 (not acceptable)- 服務端不支持所需表示
- 409 (conflict)- 通用衝突
- 412 (Precondition Failed)- 前置條件失敗(如執行條件更新時的衝突)
- 415 (unsupported media type)- 接受到的表示不受支持
- 500 (internal server error)- 通用錯誤響應
- 503 (Service Unavailable)- 服務當前無法處理請求
DELETE
- 不安全但冪等
-
刪除資源
-
200 (OK)- 資源已被刪除
- 301 (Moved Permanently)- 資源的URI已更改
- 303 (See Other)- 其他,如負載均衡
- 400 (bad request)- 指代壞請求
- 404 (not found)- 資源不存在
- 409 (conflict)- 通用衝突
- 500 (internal server error)- 通用錯誤響應
- 503 (Service Unavailable)- 服務端當前無法處理請求
狀態的轉移
有了上面的鋪墊,再討論REST裡邊的狀態轉移就會很容易理解了。 不過,我們先來討論一下REST原則中的無狀態通信原則。初看一下,好像自相矛盾了,既然無狀態,何來狀態轉移一說?
其實,這裡說的無狀態通信原則,並不是說客戶端應用不能有狀態,而是指服務端不應該保存客戶端狀態。
應用狀態與資源狀態
實際上,狀態應該區分應用狀態和資源狀態,客戶端負責維護應用狀態,而服務端維護資源狀態。 客戶端與服務端的交互必須是無狀態的,併在每一次請求中包含處理該請求所需的一切信息。 服務端不需要在請求間保留應用狀態,只有在接受到實際請求的時候,服務端才會關註應用狀態。 這種無狀態通信原則,使得服務端和中介能夠理解獨立的請求和響應。 在多次請求中,同一客戶端也不再需要依賴於同一伺服器,方便實現高可擴展和高可用性的服務端。
但有時候我們會做出違反無狀態通信原則的設計,例如利用Cookie跟蹤某個服務端會話狀態,常見的像J2EE裡邊的JSESSIONID。 這意味著,瀏覽器隨各次請求發出去的Cookie是被用於構建會話狀態的。 當然,如果Cookie保存的是一些伺服器不依賴於會話狀態即可驗證的信息(比如認證令牌),這樣的Cookie也是符合REST原則的。
應用狀態的轉移
狀態轉移到這裡已經很好理解了, “會話”狀態不是作為資源狀態保存在服務端的,而是被客戶端作為應用狀態進行跟蹤的。客戶端應用狀態在服務端提供的超媒體的指引下發生變遷。服務端通過超媒體告訴客戶端當前狀態有哪些後續狀態可以進入。 這些類似“下一頁”之類的鏈接起的就是這種推進狀態的作用–指引你如何從當前狀態進入下一個可能的狀態。
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): 修改某個已有的資源。http://tools.ietf.org/html/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的應用場景。
PUT與PATCH區別
The difference between the PUT and PATCH requests is reflected in the way the server processes the enclosed entity to modify the resource identified by the Request-URI. In a PUT request, the enclosed entity is considered to be a modified version of the resource stored on the origin server, and the client is requesting that the stored version be replaced. With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version.
The PUT method is already defined to overwrite a resource with a complete new body, and cannot be reused to do partial changes. Otherwise, proxies and caches, and even clients and servers, may get confused as to the result of the operation. POST is already used but without broad interoperability (for one, there is no standard way to discover patch format support)
A PATCH request can be issued in such a way as to be idempotent, which also helps prevent bad outcomes from collisions between two PATCH requests on the same resource in a similar time frame. Collisions from multiple PATCH requests may be more dangerous than PUT collisions because some patch formats need to operate from a known base-point or else they will corrupt the resource. Clients using this kind of patch application SHOULD use a conditional request such that the request will fail if the resource has been updated since the client last accessed the resource. For example, the client can use a strong ETag [RFC2616] in an If-Match header on the PATCH request.
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。
請查看大圖的flow的SVG
安全性
前面說過,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,HTTP Digest,OAuth,HMAC Auth, JWT等,其核心思想都是驗證某個請求是由一個合法的請求者發起。Basic Auth(http://www.ietf.org/rfc/rfc2617.txt)會把用戶的密碼暴露在網路之中,並非最安全的解決方案,OAuth的核心部分與HMAC Auth差不多,只不過多了很多與token分發相關的內容。
OAUTH 2.0
協議 https://tools.ietf.org/html/rfc6749
+--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+ Figure 1: Abstract Protocol Flow The abstract OAuth 2.0 flow illustrated in Figure 1 describes the interaction between the four roles and includes the following steps: (A) The client requests authorization from the resource owner. The authorization request can be made directly to the resource owner (as shown), or preferably indirectly via the authorization server as an intermediary. (B) The client receives an authorization grant, which is a credential representing the resource owner's authorization, expressed using one of four grant types defined in this specification or using an extension grant type. The authorization grant type depends on the method used by the client to request authorization and the types supported by the authorization server. (C) The client requests an access token by authenticating with the authorization server and presenting the authorization grant. (D) The authorization server authenticates the client and validates the authorization grant, and if valid, issues an access token.
示例參考:淘寶的OAUTH
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階段就被丟棄,最大程度保護伺服器的計算資源。
http://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
TLS
HMAC Auth儘管在保證請求的一致性上非常安全,可以用於鑒別請求是否由合法的請求者發起,但請求的數據和伺服器返回的響應都是明文傳輸,對某些要求比較高的API來說,安全級別還不夠。這時候,需要部署HTTPS。在其之上再加一層屏障。
目前應該是使用TLS1.2 https://tools.ietf.org/html/rfc5246
安全實踐提示
-
為你的API啟用其它任何你的組織已部署的web應用同樣的安全機制。比如說,如果你在web前端過濾XSS,你必須對你的API也這樣做,最好是使用同樣的工具。
-
不要使用你自己的安全。使用那些被互審過測試過的框架或現有的包...
-
除非你的API是一個免費的,只讀的公開API,否則不要使用單一的基於密鑰的驗證。這不夠,需要加上密碼要求。
-
不要放過未加密的靜態密鑰。如果你使用基本的HTTP並且線上路上發送的,請加密。
-
理想的情況下,使用基於哈布的消息驗證碼(HMAC),因為它最安全。
還可以參考REST_Security_Cheat_Sheet, Security_testing_for_REST_applications
常見問題
-
POST和PUT用於創建資源時有什麼區別?
POST和PUT在創建資源的區別在於,所創建的資源的名稱(URI)是否由客戶端決定。 例如為我的博文增加一個java的分類,生成的路徑就是分類名/categories/java,那麼就可以採用PUT方法。 不過很多人直接把POST、GET、PUT、DELETE直接對應上CRUD,例如在一個典型的rails實現的RESTFul應用中就是這麼做的。 我認為,這是因為rails預設使用服務端生成的ID作為URI的緣故,而不少人就是通過rails實踐REST的,所以很容易造成這種誤解。
-
客戶端不一定都支持這些HTTP方法吧?
的確有這種情況,特別是一些比較古老的基於瀏覽器的客戶端,只能支持GET和POST兩種方法。 在實踐上,客戶端和服務端都可能需要做一些妥協。例如rails框架就支持通過隱藏參數_method=DELETE來傳遞真實的請求方法, 而像Backbone這樣的客戶端MVC框架則允許傳遞_method傳輸和設置X-HTTP-Method-Override頭來規避這個問題。
-
統一介面是否意味著不能擴展帶特殊語義的方法?
統一介面並不阻止你擴展方法,只要方法對資源的操作有著具體的、可識別的語義即可,並能夠保持整個介面的統一性。 像WebDAV就對HTTP方法進行了擴展,增加了LOCK、UPLOCK等方法。而github的API則支持使用PATCH方法來進行issue的更新,例如:
PATCH /repos/:owner/:repo/issues/:number
不過,需要註意的是,像PATCH這種不是HTTP標準方法的,服務端需要考慮客戶端是否能夠支持的問題。
-
統一資源介面對URI有什麼指導意義?
統一資源介面要求使用標準的HTTP方法對資源進行操作,所以URI只應該來表示資源的名稱,而不應該包括資源的操作。 通俗來說,URI不應該使用動作來描述。例如,下麵是一些不符合統一介面要求的URI:
- GET /getUser/1
- POST /createUser
- PUT /updateUser/1
-
DELETE /deleteUser/1
-
如果GET請求增加計數器,這是否違反安全性?
安全性不代表請求不產生副作用,例如像很多API開發平臺,都對請求流量做限制。像github,就會限制沒有認證的請求每小時只能請求60次。 但客戶端不是為了追求副作用而發出這些GET或HEAD請求的,產生副作用是服務端“自作主張”的。 另外,服務端在設計時,也不應該讓副作用太大,因為客戶端認為這些請求是不會產生副作用的。
-
直接忽視緩存可取嗎?
即使你按各個動詞的原本意圖來使用它們,你仍可以輕易禁止緩存機制。 最簡單的做法就是在你的HTTP響應里增加這樣一個報頭: Cache-control: no-cache。 但是,同時你也對失去了高效的緩存與再驗證的支持(使用Etag等機制)。 對於客戶端來說,在為一個REST式服務實現程式客戶端時,也應該充分利用現有的緩存機制,以免每次都重新獲取表示。
-
響應代碼的處理有必要嗎?
如上圖所示,HTTP的響應代碼可用於應付不同場合,正確使用這些狀態代碼意味著客戶端與伺服器可以在一個具備較豐富語義的層次上進行溝通。 例如,201(“Created”)響應代碼表明已經創建了一個新的資源,其URI在Location響應報頭裡。 假如你不利用HTTP狀態代碼豐富的應用語義,那麼你將錯失提高重用性、增強互操作性和提升松耦合性的機會。 如果這些所謂的RESTFul應用必須通過響應實體才能給出錯誤信息,那麼SOAP就是這樣的了,它就能夠滿足了。
其他
做到了介面一致性(符合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。
今天先到這兒,希望對您有參考作用, 您可能感興趣的文章:
構建高效的研發與自動化運維
某大型電商雲平臺實踐
互聯網資料庫架構設計思路
IT基礎架構規劃方案一(網路系統規劃)
餐飲行業解決方案之客戶分析流程
餐飲行業解決方案之採購戰略制定與實施流程
餐飲行業解決方案之業務設計流程
供應鏈需求調研CheckList
企業應用之性能實時度量系統演變
如有想瞭解更多軟體設計與架構,系統 IT,企業信息化 資訊,請關註我的微信訂閱號:
作者:Petter Liu
出處:http://www.cnblogs.com/wintersun/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
該文章也同時發佈在我的獨立博客中-Petter Liu Blog。