以前一篇文章寫過REST服務介紹, 今天再次來自回顧一下. REST是一種架構風格. 首次出如今2000年Roy Fielding的博士論文中,Roy Fielding是 HTTP 規範的主要編寫者之一。 論文中提到:「我這篇文章的寫做目的,就是想在符合架構原理的前提下,理解和評估以網絡爲基礎的應用軟件的架構設計,獲得一個功能強、性能好、適宜通訊的架構。REST指的是一組架構約束條件和原則。」 若是一個架構符合REST的約束條件和原則,咱們就稱它爲RESTful架構。php
RESTFul架構應該遵循統一接口原則,統一接口包含了一組受限的預約義的操做,不論什麼樣的資源,都是經過使用相同的接口進行資源的訪問。接口應該使用標準的HTTP方法如GET,PUT和POST,並遵循這些方法的語義。
若是按照HTTP方法的語義來暴露資源,那麼接口將會擁有安全性和冪等性的特性,例如GET和HEAD請求都是安全的, 不管請求多少次,都不會改變服務器狀態。而GET、HEAD、PUT和DELETE請求都是冪等的,不管對資源操做多少次, 結果老是同樣的,後面的請求並不會產生比第一次更多的影響。html
下面列出了GET,DELETE,PUT和POST的典型用法:前端
變動時獲取表示(緩存)java
200(OK) - 表示已在響應中發出python
若是沒有被修改,則不過更新資源(樂觀鎖)git
200(OK)- 若是現有資源已被更改github
若是未被修改,則更新資源(樂觀鎖)web
200 (OK)- 若是已存在資源被更改數據庫
刪除資源express
200 (OK)- 資源已被刪除
有了上面的鋪墊,再討論REST裏邊的狀態轉移就會很容易理解了。 不過,咱們先來討論一下REST原則中的無狀態通訊原則。初看一下,好像自相矛盾了,既然無狀態,何來狀態轉移一說?
其實,這裏說的無狀態通訊原則,並非說客戶端應用不能有狀態,而是指服務端不該該保存客戶端狀態。
實際上,狀態應該區分應用狀態和資源狀態,客戶端負責維護應用狀態,而服務端維護資源狀態。 客戶端與服務端的交互必須是無狀態的,並在每一次請求中包含處理該請求所需的一切信息。 服務端不須要在請求間保留應用狀態,只有在接受到實際請求的時候,服務端纔會關注應用狀態。 這種無狀態通訊原則,使得服務端和中介可以理解獨立的請求和響應。 在屢次請求中,同一客戶端也再也不須要依賴於同一服務器,方便實現高可擴展和高可用性的服務端。
但有時候咱們會作出違反無狀態通訊原則的設計,例如利用Cookie跟蹤某個服務端會話狀態,常見的像J2EE裏邊的JSESSIONID。 這意味着,瀏覽器隨各次請求發出去的Cookie是被用於構建會話狀態的。 固然,若是Cookie保存的是一些服務器不依賴於會話狀態便可驗證的信息(好比認證令牌),這樣的Cookie也是符合REST原則的。
狀態轉移到這裏已經很好理解了, 「會話」狀態不是做爲資源狀態保存在服務端的,而是被客戶端做爲應用狀態進行跟蹤的。客戶端應用狀態在服務端提供的超媒體的指引下發生變遷。服務端經過超媒體告訴客戶端當前狀態有哪些後續狀態能夠進入。 這些相似「下一頁」之類的連接起的就是這種推動狀態的做用–指引你如何從當前狀態進入下一個可能的狀態。
REST API通常用來將某種資源和容許的對資源的操做暴露給外界,使調用者可以以正確的方式操做資源。這裏,在輸入輸出的處理上,要符合HTTP/1.1(不久的未來,要符合HTTP/2.0)的RFC,保證接口的一致性。這裏主要講輸入的method/headers和輸出的status code。
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的應用場景。
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.
不少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。這個頭很是重要,下文詳解。
不少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分發相關的內容。
+--------+ +---------------+ | |--(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
回到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
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在建立資源的區別在於,所建立的資源的名稱(URI)是否由客戶端決定。 例如爲個人博文增長一個java的分類,生成的路徑就是分類名/categories/java,那麼就能夠採用PUT方法。 不過不少人直接把POST、GET、PUT、DELETE直接對應上CRUD,例如在一個典型的rails實現的RESTFul應用中就是這麼作的。 我認爲,這是由於rails默認使用服務端生成的ID做爲URI的緣故,而很多人就是經過rails實踐REST的,因此很容易形成這種誤解。
的確有這種狀況,特別是一些比較古老的基於瀏覽器的客戶端,只能支持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標準方法的,服務端須要考慮客戶端是否可以支持的問題。
統一資源接口要求使用標準的HTTP方法對資源進行操做,因此URI只應該來表示資源的名稱,而不該該包括資源的操做。 通俗來講,URI不該該使用動做來描述。例如,下面是一些不符合統一接口要求的URI:
DELETE /deleteUser/1
安全性不表明請求不產生反作用,例如像不少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。