場景:由於本身作的後臺,一直有爲前端提供接口,一直知道用的是restful框架,但是沒有深刻理解,先整理以下。html
轉載自:http://kb.cnblogs.com/page/512047/前端
http://kb.cnblogs.com/page/521718/java
1. 什麼是RESTgit
REST全稱是Representational State Transfer,中文意思是表述(編者注:一般譯爲表徵)性狀態轉移。 它首次出如今2000年Roy Fielding的博士論文中,Roy Fielding是HTTP規範的主要編寫者之一。 他在論文中提到:「我這篇文章的寫做目的,就是想在符合架構原理的前提下,理解和評估以網絡爲基礎的應用軟件的架構設計,獲得一個功能強、性能好、適宜通訊的架構。REST指的是一組架構約束條件和原則。」 若是一個架構符合REST的約束條件和原則,咱們就稱它爲RESTful架構。github
REST自己並無創造新的技術、組件或服務,而隱藏在RESTful背後的理念就是使用Web的現有特徵和能力, 更好地使用現有Web標準中的一些準則和約束。雖然REST自己受Web技術的影響很深, 可是理論上REST架構風格並非綁定在HTTP上,只不過目前HTTP是惟一與REST相關的實例。 因此咱們這裏描述的REST也是經過HTTP實現的REST。web
2. 理解RESTful算法
要理解RESTful架構,須要理解Representational State Transfer這個詞組究竟是什麼意思,它的每個詞都有些什麼涵義。 下面咱們結合REST原則,圍繞資源展開討論,從資源的定義、獲取、表述、關聯、狀態變遷等角度,列舉一些關鍵概念並加以解釋。數據庫
2. 1 資源與URIjson
REST全稱是表述性狀態轉移,那究竟指的是什麼的表述? 其實指的就是資源。任何事物,只要有被引用到的必要,它就是一個資源。資源能夠是實體(例如手機號碼),也能夠只是一個抽象概念(例如價值) 。下面是一些資源的例子:api
要讓一個資源能夠被識別,須要有個惟一標識,在Web中這個惟一標識就是URI(Uniform Resource Identifier)。 URI既能夠當作是資源的地址,也能夠當作是資源的名稱。若是某些信息沒有使用URI來表示,那它就不能算是一個資源, 只能算是資源的一些信息而已。URI的設計應該遵循可尋址性原則,具備自描述性,須要在形式上給人以直覺上的關聯。這裏以github網站爲例,給出一些還算不錯的URI:
下面讓咱們來看看URI設計上的一些技巧:
曾經Web上的URI都是冰冷的數字或者無心義的字符串,但如今愈來愈多的網站使用_或-來分隔一些單詞,讓URI看上去更爲人性化。 例如國內比較出名的開源中國社區,它上面的新聞地址就採用這種風格, 如http://www.oschina.net/news/38119/oschina-translate-reward-plan。
例如上述/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08就表示了一個多級的資源, 指的是git用戶的git項目的某次提交記錄,又例如/orders/2012/10能夠用來表示2012年10月的訂單記錄。
不少人只是把?簡單的當作是參數的傳遞,很容易形成URI過於複雜、難以理解。能夠把?用於對資源的過濾, 例如/git/git/pulls用來表示git項目的全部推入請求,而/pulls?state=closed用來表示git項目中已經關閉的推入請求, 這種URL一般對應的是一些特定條件的查詢結果或算法運算結果。
有時候咱們須要表示同級資源的關係時,可使用,或;來進行分割。例如哪天github能夠比較某個文件在隨意兩次提交記錄之間的差別,或許可使用/git/git /block-sha1/sha1.h/compare/e3af72cdafab5993d18fae056f87e1d675913d08;bd63e61bdf38e872d5215c07b264dcc16e4febca做爲URI。 不過,如今github是使用…來作這個事情的,例如/git/git/compare/master…next。
2. 2 統一資源接口
RESTful架構應該遵循統一接口原則,統一接口包含了一組受限的預約義的操做,不論什麼樣的資源,都是經過使用相同的接口進行資源的訪問。接口應該使用標準的HTTP方法如GET,PUT和POST,並遵循這些方法的語義。
若是按照HTTP方法的語義來暴露資源,那麼接口將會擁有安全性和冪等性的特性,例如GET和HEAD請求都是安全的, 不管請求多少次,都不會改變服務器狀態。而GET、HEAD、PUT和DELETE請求都是冪等的,不管對資源操做多少次, 結果老是同樣的,後面的請求並不會產生比第一次更多的影響。
下面列出了GET,DELETE,PUT和POST的典型用法:
GET
POST
PUT
DELETE
下面咱們來看一些實踐中常見的問題:
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:
若是GET請求增長計數器,這是否違反安全性?
安全性不表明請求不產生反作用,例如像不少API開發平臺,都對請求流量作限制。像github,就會限制沒有認證的請求每小時只能請求60次。 但客戶端不是爲了追求反作用而發出這些GET或HEAD請求的,產生反作用是服務端「自做主張」的。 另外,服務端在設計時,也不該該讓反作用太大,由於客戶端認爲這些請求是不會產生反作用的。
即便你按各個動詞的本來意圖來使用它們,你仍能夠輕易禁止緩存機制。 最簡單的作法就是在你的HTTP響應裏增長這樣一個報頭: Cache-control: no-cache。 可是,同時你也對失去了高效的緩存與再驗證的支持(使用Etag等機制)。 對於客戶端來講,在爲一個REST式服務實現程序客戶端時,也應該充分利用現有的緩存機制,以避免每次都從新獲取表示。
HTTP的響應代碼可用於應付不一樣場合,正確使用這些狀態代碼意味着客戶端與服務器能夠在一個具有較豐富語義的層次上進行溝通。 例如,201(「Created」)響應代碼代表已經建立了一個新的資源,其URI在Location響應報頭裏。 假如你不利用HTTP狀態代碼豐富的應用語義,那麼你將錯失提升重用性、加強互操做性和提高鬆耦合性的機會。 若是這些所謂的RESTful應用必須經過響應實體才能給出錯誤信息,那麼SOAP就是這樣的了,它就可以知足了。
2. 3 資源的表述
上面提到,客戶端經過HTTP方法能夠獲取資源,是吧? 不,確切的說,客戶端獲取的只是資源的表述而已。 資源在外界的具體呈現,能夠有多種表述(或成爲表現、表示)形式,在客戶端和服務端之間傳送的也是資源的表述,而不是資源自己。 例如文本資源能夠採用html、xml、json等格式,圖片可使用PNG或JPG展示出來。 資源的表述包括數據和描述數據的元數據,例如,HTTP頭「Content-Type」 就是這樣一個元數據屬性。
那麼客戶端如何知道服務端提供哪一種表述形式呢?
答案是能夠經過HTTP內容協商,客戶端能夠經過Accept頭請求一種特定格式的表述,服務端則經過Content-Type告訴客戶端資源的表述形式。
以github爲例,請求某組織資源的json格式的表述形式:
假如github也可以支持xml格式的表述格式,那麼結果就是這樣的:
下面咱們來看一些實踐上常見的設計:
在URI裏邊帶上版本號
有些API在URI裏邊帶上版本號,例如:
若是咱們把版本號理解成資源的不一樣表述形式的話,就應該只是用一個URL,並經過Accept頭部來區分,仍是以github爲例,它的Accept的完整格式是:application/vnd.github[.version].param[+json]
對於v3版本的話,就是Accept: application/vnd.github.v3。對於上面的例子,同理可使用使用下面的頭部:
使用URI後綴來區分表述格式
像rails框架,就支持使用/users.xml或/users.json來區分不一樣的格式。 這樣的方式對於客戶端來講,無疑是更爲直觀,但混淆了資源的名稱和資源的表述形式。 我我的認爲,仍是應該優先使用內容協商來區分表述格式。
如何處理不支持的表述格式
當服務器不支持所請求的表述格式,那麼應該怎麼辦?若服務器不支持,它應該返回一個HTTP 406響應,表示拒絕處理該請求。下面以github爲例,展現了一個請求XML表述資源的結果:
2. 4 資源的連接
咱們知道REST是使用標準的HTTP方法來操做資源的,但僅僅所以就理解成帶CURD的Web數據庫架構就太過於簡單了。 這種反模式忽略了一個核心概念:「超媒體即應用狀態引擎(hypermedia as the engine of application state)」。 超媒體是什麼? 當你瀏覽Web網頁時,從一個鏈接跳到一個頁面,再從另外一個鏈接跳到另一個頁面,就是利用了超媒體的概念:把一個個把資源連接起來.
要達到這個目的,就要求在表述格式裏邊加入連接來引導客戶端。在《RESTful Web Services》一書中,做者把這種具備連接的特性成爲連通性。下面咱們具體來看一些例子。
下面展現的是github獲取某個組織下的項目列表的請求,能夠看到在響應頭裏邊增長Link頭告訴客戶端怎麼訪問下一頁和最後一頁的記錄。 而在響應體裏邊,用url來連接項目全部者和項目地址。
又例以下面這個例子,建立訂單後經過連接引導客戶端如何去付款。
上面的例子展現瞭如何使用超媒體來加強資源的連通性。不少人在設計RESTful架構時,使用不少時間來尋找漂亮的URI,而忽略了超媒體。因此,應該多花一些時間來給資源的表述提供連接,而不是專一於「資源的CRUD」。
2. 5 狀態的轉移
有了上面的鋪墊,再討論REST裏邊的狀態轉移就會很容易理解了。 不過,咱們先來討論一下REST原則中的無狀態通訊原則。初看一下,好像自相矛盾了,既然無狀態,何來狀態轉移一說?
其實,這裏說的無狀態通訊原則,並非說客戶端應用不能有狀態,而是指服務端不該該保存客戶端狀態。
2. 5.1 應用狀態與資源狀態
實際上,狀態應該區分應用狀態和資源狀態,客戶端負責維護應用狀態,而服務端維護資源狀態。 客戶端與服務端的交互必須是無狀態的,並在每一次請求中包含處理該請求所需的一切信息。 服務端不須要在請求間保留應用狀態,只有在接受到實際請求的時候,服務端纔會關注應用狀態。 這種無狀態通訊原則,使得服務端和中介可以理解獨立的請求和響應。 在屢次請求中,同一客戶端也再也不須要依賴於同一服務器,方便實現高可擴展和高可用性的服務端。
但有時候咱們會作出違反無狀態通訊原則的設計,例如利用Cookie跟蹤某個服務端會話狀態,常見的像J2EE裏邊的JSESSIONID。 這意味着,瀏覽器隨各次請求發出去的Cookie是被用於構建會話狀態的。 固然,若是Cookie保存的是一些服務器不依賴於會話狀態便可驗證的信息(好比認證令牌),這樣的Cookie也是符合REST原則的。
2. 5.2 應用狀態的轉移
狀態轉移到這裏已經很好理解了, 「會話」狀態不是做爲資源狀態保存在服務端的,而是被客戶端做爲應用狀態進行跟蹤的。客戶端應用狀態在服務端提供的超媒體的指引下發生變遷。服務端經過超媒體告訴客戶端當前狀態有哪些後續狀態能夠進入。 這些相似「下一頁」之類的連接起的就是這種推動狀態的做用——指引你如何從當前狀態進入下一個可能的狀態。
3.撰寫合格的rest api
REST API通常用來將某種資源和容許的對資源的操做暴露給外界,使調用者可以以正確的方式操做資源。這裏,在輸入輸出的處理上,要符合HTTP/1.1(不久的未來,要符合HTTP/2.0)的RFC,保證接口的一致性。這裏主要講輸入的method/headers和輸出的status code。
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的應用場景。
不少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暴露給第三方,不但貽笑大方,還會留下諸多互操做上的隱患。
安全性
前面說過,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階段就被丟棄,最大程度保護服務器的計算資源。
HMAC Auth儘管在保證請求的一致性上很是安全,能夠用於鑑別請求是否由合法的請求者發起,但請求的數據和服務器返回的響應都是明文傳輸,對某些要求比較高的API來講,安全級別還不夠。這時候,須要部署HTTPS。在其之上再加一層屏障。