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