1.概述
本文將重點介紹ETags-Spring支持,RESTful API的集成測試以及帶有curl的使用場景。 這是關于使用Spring 3.1和Spring Security 3.1和基于Java的配置來建立安全的RESTful Web服務的系列文章的第9篇。
REST with Spring系列:
- 第1部分 – 使用Spring 3.1和基于Java的配置引導Web應用程序
- P藝術2 - 構建RESTful Web服務使用Spring 3.1和Java配置
- P藝術3 - 保護RESTful Web服務使用Spring Security 3.1
- 第4部分 – RESTful Web服務可發現性
- 第5部分 – 使用Spring進行REST服務發現
- 第6部分 – 使用Spring Security 3.1的RESTful服務的基本身份驗證和摘要身份驗證
- 第7部分 – Spring的REST分頁
- 第8部分 – 使用Spring Security對RESTful服務進行身份驗證
2. REST和ETag
從有關ETag支持的Spring官方文檔中:
ETag (實體標簽)是由HTTP / 1.1兼容的Web服務器返回的HTTP響應標頭,用于確定給定URL的內容更改。ETag用于兩件事–緩存和條件請求。 ETag值可以是從Response主體的字節中計算得出的哈希值 。 因為很可能使用了加密哈希函數,所以即使是主體的最小修改也將極大地改變輸出,從而改變ETag的值。 這僅適用于強大的ETag-該協議的確也提供了較弱的Etag 。
使用If- *標頭會將標準GET請求轉換為條件GET 。 與ETag一起使用的兩個If- *標頭是“ If-None-Match ”和“ If-Match ” –各自具有自己的語義,如本文稍后所述。
3.使用
涉及ETag的簡單的Client-Server通信可以分為以下步驟:
– 首先 ,客戶端進行REST API調用–響應包括要存儲以供進一步使用的ETag標頭:
curl -H 'Accept: application/json' -i http://localhost:8080/rest-sec/api/resources/1
HTTP/1.1 200 OK
ETag: 'f88dd058fe004909615a64f01be66a7'
Content-Type: application/json;charset=UTF-8
Content-Length: 52
–客戶端對RESTful API發出的下一個請求包括帶有上一步中的ETag值的If-None-Match請求標頭; 如果服務器上的資源未更改,則響應將不包含任何正文,并且狀態代碼為304 –未修改 :
curl -H 'Accept: application/json' -H 'If-None-Match: 'f88dd058fe004909615a64f01be66a7''-i http://localhost:8080/rest-sec/api/resources/1
HTTP/1.1 304 Not Modified
ETag: 'f88dd058fe004909615a64f01be66a7'
– 現在 ,在再次檢索資源之前,我們將通過執行更新來對其進行更改:
curl --user admin@fake.com:adminpass -H 'Content-Type: application/json' -i-X PUT --data '{ 'id':1, 'name':'newRoleName2', 'description':'theNewDescription' }'
http://localhost:8080/rest-sec/api/resources/1
HTTP/1.1 200 OK
ETag: 'd41d8cd98f00b204e9800998ecf8427e'
<strong>Content-Length: 0</strong>
– 最后 ,我們發出了最后一個請求以再次獲取特權; 請記住,自上次檢索以來已對其進行了更新,因此先前的ETag值將不再起作用-響應將包含新數據和新ETag,這些ETag可以再次存儲以備后用:
curl -H 'Accept: application/json' -H 'If-None-Match: 'f88dd058fe004909615a64f01be66a7'' -i
http://localhost:8080/rest-sec/api/resources/1
HTTP/1.1 200 OK
ETag: '03cb37ca667706c68c0aad4cb04c3a211'
Content-Type: application/json;charset=UTF-8
Content-Length: 56
一切就在這里– ETags狂野地節省了帶寬。
4. Spring對ETag的支持
對Spring的支持–在Spring中使用ETag非常容易設置,并且對于應用程序是完全透明的。 通過在web.xml中添加一個簡單的Filter來啟用該支持:
<filter><filter-name>etagFilter</filter-name><filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>
<filter-mapping><filter-name>etagFilter</filter-name><url-pattern>/api/*</url-pattern>
</filter-mapping>
篩選器映射到與RESTful API本身相同的URI模式。 從Spring 3.0開始,過濾器本身就是ETag功能的標準實現。
該實現是一個淺層實現-根據響應計算ETag,這將節省帶寬,但不能 節省 服務器性能 。 因此,將從ETag支持中受益的請求仍將作為標準請求進行處理,消耗其通常會消耗的任何資源(數據庫連接等),并且只有在將其響應返回給客戶端之前,ETag支持才會啟動在。
屆時,ETag將從響應主體中計算出來,并在資源本身上設置; 同樣,如果在請求中設置了If-None-Match標頭,則也會對其進行處理。
ETag機制的更深層實現可能會帶來更大的好處-例如為緩存中的某些請求提供服務,而根本不必執行計算-但這種實現絕非像淺層方法那樣簡單,也不可插入在這里描述。
5.測試ETag
讓我們開始簡單–我們需要驗證檢索單個Resource的簡單請求的響應是否實際上將返回“ ETag”標頭:
@Test
public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() {// GivenResource existingResource = getApi().create(new Resource());String uriOfResource = baseUri + '/' + existingResource.getId();// WhenResponse findOneResponse = RestAssured.given().header('Accept', 'application/json').get(uriOfResource);// ThenassertNotNull(findOneResponse.getHeader(HttpHeaders.ETAG));
}
接下來 , 我們驗證ETag行為的幸福路徑 –如果從服務器檢索資源的請求使用正確的ETag值,則不再返回資源。
@Test
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {// GivenT existingResource = getApi().create(createNewEntity());String uriOfResource = baseUri + '/' + existingResource.getId();Response findOneResponse = RestAssured.given().header('Accept', 'application/json').get(uriOfResource);String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);// WhenResponse secondFindOneResponse= RestAssured.given().header('Accept', 'application/json').headers('If-None-Match', etagValue).get(uriOfResource);// ThenassertTrue(secondFindOneResponse.getStatusCode() == 304);
}
一步步:
- 首先創建資源 ,然后再檢索–保存ETag值以備將來使用
- 發送新的檢索請求,這次使用“ If-None-Match ”標題指定先前存儲的ETag值
- 在第二個請求上,服務器僅返回304 Not Modified ,因為資源本身在兩次檢索操作之間確實沒有被修改。
最后 ,我們驗證在第一個和第二個檢索請求之間更改資源的情況:
@Test
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {// GivenT existingResource = getApi().create(createNewEntity());String uriOfResource = baseUri + '/' + existingResource.getId();Response findOneResponse = RestAssured.given().header('Accept', 'application/json').get(uriOfResource);String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);existingResource.setName(randomAlphabetic(6))getApi().update(existingResource.setName(randomString));// WhenResponse secondFindOneResponse= RestAssured.given().header('Accept', 'application/json').headers('If-None-Match', etagValue).get(uriOfResource);// ThenassertTrue(secondFindOneResponse.getStatusCode() == 200);
}
一步步:
- 首先創建資源 ,然后再檢索–保存ETag值以備將來使用
- 然后更新相同的資源
- 發送新的檢索請求,這次使用“ If-None-Match ”標題指定先前存儲的ETag值
- 在第二個請求上,服務器將返回200 OK以及完整的Resource,因為ETag值不再正確,因為與此同時資源已更新
接下來 ,我們測試“ If-Match ”的行為– ShallowEtagHeaderFilter沒有為If-Match HTTP標頭提供開箱即用的支持(在此JIRA問題上進行了跟蹤),因此以下測試應失敗:
@Test
public void givenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived() {// GivenT existingResource = getApi().create(createNewEntity());// WhenString uriOfResource = baseUri + '/' + existingResource.getId();Response findOneResponse = RestAssured.given().header('Accept', 'application/json').headers('If-Match', randomAlphabetic(8)).get(uriOfResource);// ThenassertTrue(findOneResponse.getStatusCode() == 412);
}
一步步:
- 首先創建資源
- 然后使用“ If-Match ”標題檢索的資源指定了錯誤的ETag值-這是一個有條件的GET請求
- 服務器應返回412前提條件失敗
6. ETag很大
我們僅將ETag用于讀取操作 – 存在RFC,試圖闡明實現方式應如何在寫入操作中處理ETag –這不是標準的,但很有趣。
當然,ETag機制還有其他可能的用途,例如用于使用Spring 3.1的樂觀鎖定機制以及處理相關的“丟失更新問題” 。
使用ETag時,還需要注意一些已知的潛在陷阱和警告 。
7.結論
本文僅介紹了Spring和ETags所能提供的功能。 要全面實現啟用了ETag的RESTful服務,以及用于驗證ETag行為的集成測試,請查看github項目 。
參考:來自badung博客的JCG合作伙伴 Eugen Paraschiv 提供的Spring的ETags 。
翻譯自: https://www.javacodegeeks.com/2013/01/etags-for-rest-with-spring.html