最近还是继续在学习 REST、 微服务等相关知识,想从理论方面对这些知识有个更深入、更全面的了解。《RESTful Web Services 中文版》是目前正在看的。对其中有些内容作了摘录,便于以后查阅和复习。 其中,引用的书中的内容,版权归作者所有。如有侵权,请联系删除。
第五章 设计只读的面向资源的服务
统一接口(uniform interface)意味着,在面向对象设计里被视为动词(verb)的事物,在面向资源的设计里必须被视为对象(object)。在 ROA 里,一个读者(Reader)不能“订阅”一个栏目(Column),因为“订阅(subscribe to)”不属于统一接口里的方法。必须通过另一个订阅(Subscription)对象来代表读者与栏目的关系。这种关系对象(relationship object)是符合统一接口的:它可被创建、获取(比如通过聚合的方式)和删除。在面向对象分析里,“订阅(Subscription)”也许不是一类对象(first-class object),而是下层数据库模型里的一张表。在面向资源的分析中,所有的对象处理(object manupulation)都是通过符合统一接口的资源来实现的。如果要给我的资源添加新方法,就要为此定义一个新资源。
创建一组响应 GET 和 HEAD(HTTP 统一接口的一个只读子集)的资源的步骤:
-
规划数据集
-
把数据集划分为资源
记住,一个资源就是任何值得作为超链接目标的事物。任何可能用名称来引用的事物都应该有自己的名称。服务暴露的资源可以分为三类:
- 为特别目的专门预定义的一次性资源
这包括其他可用资源的最上层目录。大多数服务几乎不暴露一次性资源(one-off resources)。 - 服务暴露的每一个对象做对应的资源
一个服务可以暴露很多对象,每一种都有自己的资源集合。大多数服务暴露很多(甚至无数)个这样的资源。 - 代表在数据集上执行算法的结果的资源
这包括作为查询结果的集合资源。对于大多数服务,此类资源不是有无数个,就是一个也没有。
REST 式 Web 服务通过资源(resource)来暴露数据(data)和算法(algorithms)。有关数据的资源常常构成一个层次结构(hierarchy):由很少的资源开始,然后逐渐扩展为具有许多叶节点。
- 为特别目的专门预定义的一次性资源
-
用 URI 为该资源命名
URI 能够很自然地把作用域信息组织起来。URI 设计有三条基本原则:
- 用路径变量(path variables)来表达层次结构(hierarchy):/parent/child。
- 在路径变量里加上标点符号,以消除误解:/parent/child1;child2。
用逗号或分号表达非层次结构。建议:当作用域信息的次序重要时,就用逗号,否则就用分号。关于在 URI 里使用分号,还有一种矩阵 URI (matrix URIs),在 URI 里定义键值对。 - 用查询变量(query variables)来表达算法的输入,例如:/search?q=jellyfish&start=20。
-
设计发给客户端的表示
表示(representation)采用的格式要能够:(1)传达资源的当前状态;(2)链接到可能的下个应用状态或资源状态。
传达资源状态是表示的主要用途。“资源状态(resource state)”就是有关资源的任何信息。
表示的另一个用途是推进状态。一个资源的表示应该链接到邻近的资源,比如可能的下个应用状态(application state)。这样做是为了实现连通性(connectedness)——通过跟随链接(following links)从一个资源到另一个资源的能力。 -
用超链接和表单把该资源与已有资源联系起来
客户端搜索的结果可能有无数种,但是我们不可能为所有搜索结果都专门列出一个链接。HTML 通过表单(Form)来解决这种问题。我们可以通过在表示里提供一个表单,告诉客户端如何在查询字符串(query string)里带入变量。该表单代表着无数个遵循一定模式的 URIs。
-
考虑有哪些典型的事件经过
许多只读资源的典型事件经过(course of events)都是比较简单的。用户向一个 URI 发出 GET 请求,服务器返回正确的响应代码(比如 200)、一些 HTTP 报头及一个实体主体(表示)。HEAD 请求的过程也差不多,只是服务器不发送实体主体(表示)而已。这里最重要的问题是:客户端的请求和服务器的响应里分别应包含哪些 HTTP 报头。
条件 HTTP GET
条件 HTTP GET(conditional HTTP GET)可以节省客户端和服务器的时间与带宽。它是通过两个响应报头(Last-Modified 和 ETag)和两个请求报头(If-Modified-Since 和 If-None-Match)实现的。 -
考虑可能出现哪些错误情况
- 如果不存在,响应代码 404(”Not Found”),无需在 HTTP 响应里提供实体主体。
- 如果有相近的资源,响应代码 303(”See Other”),并在 HTTP 响应报头 Location 里给出这个资源的 URI。客户端可以自己决定是否采纳这个建议,以及是否请求那个 URI。假设没有相近的,再返回响应代码 404(”Not Found”)。
- 如果客户端请求有错,应该确切的响应代码 400 (”Bad Request”)。
- 服务器也许会因为请求太多而过载,无法处理当前请求。对于这种情况,响应代码应该是 503(”Service Unavailable”)。还有一个选择,就是拒绝处理该请求。
- 服务器可能会运行出错。这可能是因为数据丢失或数据错误、软件 bug、硬件故障等原因造成的。对于这种情况,响应代码应该是 500(”Inernal Server Error”)。
第六章 设计可读写的面向资源的服务
客户端使用一个 Web 服务,是因为该服务能够提供它所需的数据、算法或数据存储空间。Web 服务是一个抽象层,就如同操作系统 API 或编程语言库一样。假如很多用户都在你的服务之上实现同一种功能,那么可以把这个功能放在服务里实现,以免去他们自己实现的麻烦。如果很多用户都想往数据集里增添某种自定义数据,那么你可以增加一种新资源,这样他们就不必定义自己的局部结构了。
认证(authentication)要做的,就是把请求跟用户关联起来。授权(authorization)要做的,就是确定一个给定用户可以做哪些请求。
在第八章会对 REST 式认证与授权模式做详细介绍,不过这里先做一个基本介绍。当 Web 服务客户端发出一个 HTTP 请求时,它将在 Authorization 报头里附上一些证书(credentials)。在服务收到请求后,它会通过检查这些证书来确定客户端是否代表某一特定账户(认证),以及该用户是否被允许做它所请求的操作(授权)。若这两个条件都能满足,那么服务将处理该请求;若请求里没有有效的证书,或者该证书未能通过授权,那么服务器将发送响应代码 401(”Unauthorized”),并在 WWW-Authenticate 响应报头里指出如何发送有效的证书。
标准的认证方案有好几种,最常见的有:HTTP 基本认证(HTTP Basic)、HTTP 摘要认证(HTTP Digest)和 WSSE 认证。有些 Web 服务采用自己的专有认证方法。
采用 SSL 对 HTTP 进行加密。采用 HTTPS 可以防止其他计算机窃听客户端与服务器之间的对话。采用 HTTP 基本认证时这点尤为重要,因为在这种认证机制下,客户端是通过纯文本发送证书的。
创建读写资源的步骤:
-
规划数据集
-
把数据集划分为资源
-
用 URI 为该资源命名
-
暴露一个统一接口的子集
可以通过回答以下问题来决定暴露统一接口的哪个子集:
- 客户端将创建这种新资源吗?
- 当客户端创建这种新资源时,谁负责决定这个新资源的 URI?客户端还是服务器?
- 客户端将修改这种资源吗?
- 客户端将删除这种资源吗?
- 客户端将获取这种资源的表示吗?
-
设计来自客户端的表示
表单编码
这种表示没有官方的名称,它只有媒体类型(application/x-www-form-urlencoded)。它有时被称为“CGI 转义(CGI escaping)”。当你通过 Web 浏览器提交 HTML 表单时,浏览器就是用这种格式把表单数据编组(marshal)为“可以放在 HTTP 请求里的格式”的。
当一个对象的状态可以表达为键-值对(key-value pairs)时,表达编码(form-encoding)是最简单的表示格式。差不多所有编程语言都为表单编码及解码提供内在支持。 -
设计发给客户端的表示
-
用超链接和表单把该资源与已有资源联系起来
-
考虑有哪些典型的时间经过
若创建成功,返回响应代码 201(”Created”),Location 报头里包含新创建的资源的 URI。
若修改成功,返回响应代码 200(”OK”),并在响应主体里包含资源的表示。若修改不影响资源的表示,也可以把响应代码设为 205 (”Reset Content”),并省略响应实体主体。若修改导致了资源的 URI 改变,那么应该返回响应代码 301(”Moved Permanently”),并在 Location 报头里给出新的 URI。
删除一个资源后,服务器应该返回代码 200(”OK”)。 -
考虑可能出现哪些错误情况
服务端无法理解客户端的表示时,响应代码是 415(”Unsupported Media Type”)。
如果客户端没有附上表示,或者表示的格式错误,这种情况下响应代码应该是 400(”Bad Request”),还有 409(”Conflict”)的情况。
如果客户端提供了不正确的证书,或者根本没有提供 Authorization 报头。对于这种情况,响应代码应该是 401(”Unauthorized”),并设置适当的 WWW-Authenticate 响应报头、以告诉客户端如何按照 HTTP 基本认证的规则(书中例子用的是 HTTP 基本认证方式)来格式化 Authorization 报头。
另外还有可能会出现 500(”Internal Server Error”)和 503(”Service Unavailable”)错误。
第七章 一个服务实现
是一个用 Ruby On Rails 写的一个网络书签
第八章 REST 和 ROA 最佳实践
-
一般的 ROA 设计步骤,跟第六章的步骤一样。
-
可寻址性,如果一个 Web 服务将其数据集里有价值部分作为资源(Resource)发布出来,那么该应用就是可寻址的(addressable)。
-
表示应当是可寻址的,一个 URI 应当只标识一个资源,否则它就不是统一资源标识符了。另外,我建议给一个资源的不同表示分别分配不同的 URI。因为 URI 经常被传来传去,或者作为其他 Web 服务的输入,所以 URI 应标识一个资源的特定表示。
客户端用 HTTP 请求报头来发送信息,这是可以的,只要服务器不是仅靠其中的信息来选择资源或表示就行。报头里也可以包含一些敏感信息,如认证证书等。但报头不应是唯一“被客户端用来指定请求哪个资源或表示”的地方。 -
状态与无状态,资源状态(Resource State)保存在服务端,而且只能以表示(representation)的形式发给客户端。应用状态(application state)保存在客户端;当它能够用于创建、修改或删除一个资源时,它将作为 POST, PUT 或 DELETE 请求的一部分发送给服务器,成为资源状态。
若一个 REST 式服务从不保存任何应用状态,那么就称它为“无状态的(stateless)”。在一个无状态的应用里,服务是按当前的资源状态来独立的处理各个客户端请求的。假如客户端希望服务器在处理请求时参考某个应用状态,那么客户端必须把这个应用状态作为请求的一部分发给服务器。比方说,客户端为每个请求附上认证证书。
客户端通过“在 PUT 或 POST 请求里附加一个表示(representation)”来对资源状态进行处理(DELETE 请求也差不多,只是没有表示而已),而服务器通过“在响应客户端的 GET 请求时附上表示”来处理应用状态——这正是“表示性状态转移(REpresentational State Transfer, REST)”这个词的由来。 -
连通性,服务器可以通过“在表示里给出链接与表单(links and forms)”来引导应用状态的变迁。“超媒体作为应用状态的引擎(Hypermedia as the engine of application state, HATEOAS)”
-
统一接口,GET, HEAD, PUT, POST, DELETE, OPTION 各个 HTTP 基本方法的通用用法。
安全性与幂等性,GET 请求和 HEAD 请求应该是安全的:它们不应该导致服务器状态发生任何改变。PUT 和 DELETE 请求应当是幂等的。应当避免用 PUT 请求对资源状态做相对的改变,比方说“把 value 值加 5”。因为这样的话,发送 10 PUT 请求,将跟只做一次请求存在巨大的差别。PUT 请求应该把资源状态设为特定的值。GET 和 HEAD 请求,天然具备幂等性。POST 请求,既不是安全的,也不是幂等的。对于重载的 POST 请求,它是否具有安全性、幂等性是不一定的。
新建资源 PUT vs. POST,你可以允许客户端通过 PUT 或/和 POST来创建资源。只有当客户端能够自己构造出新资源的 URI 时,才能使用 PUT 来创建资源。如果让服务器为新资源分配 URI,那就使用 POST 请求来创建资源。
重载POST,重载的 POST 有两个无可争议的用途。一是用于为 Web 浏览器这种不支持 PUT 和 DELETE 的客户端(现在最新的浏览器都支持)模拟 HTTP 统一接口。二是用于绕过 URI 的最大长度限制。
-
资源设计,可以把任何数据或算法暴露为一个资源。资源可分为 3 类。
-
预定义的一次性(one-off)资源。比如服务的主页,或者指向其他资源的静态链接列表。这类资源的数量不多,它可能是面向对象系统里的一个类(class),或者面向数据库系统里的一张数据库表(database table)。
-
大量(或无数个)对应于各数据项的资源。此类资源可能对应于面向对象系统里的一个对象(object),或者面向数据库的系统里的一条数据库记录(database row)。
-
大量(或无数个)对应于一个可能的算法输出结果的资源。此类资源可能对应于一个面向数据库的系统里的查询结果。搜索结果列表和已过滤的资源列表均属此类资源。
异步操作。响应代码 202(”Accepted”),创建新的 job 资源。
批量操作。假如要批量创建资源,可以暴露一个工厂资源,它可以接受包含一组资源表示的文档,并根据这些表示创建多个资源。要是想一次修改或删除多个资源,可以在 URI 里包含多个 URI 路径,甚至多个完整的 URI(只要对它们作转义处理即可)。响应代码用 202(”Accepted”),或者采用 WebDAV 的扩展 HTTP 响应代码:207(”Multi-Status”)。
事务。把事务本身暴露为资源。REST 式事务,要比数据库事务或者编程语言事务实现起来更复杂:事务中的每一步都来自单独的 HTTP 请求,每一步都要标识一个资源,每一步都要符合统一接口。 -
-
URI 设计,URI 应该具有一定的意义和良好的结构,以便客户端能够自己构造 URI 来访问它们想访问的资源。
在设计 URI 时,路径变量被用于分隔一个层次结构或有向图的元素。
标点符号被用于在同一层次上分隔多项数据。如果各项数据的次序有关紧要,就采用逗号。如果次序无关紧要,就采用分号。
若路径变量和标点符号均解决不了问题,或者如果你要往一个算法里代入参数的话,那么可以采用查询变量。如果两个 URI 只在查询变量上有差别,这表明它们只是为同一算法设置了不同的参数。
URI 标识的是资源(Resource),而不是对资源的操作(Operation)。所以说,把操作名称放在 URI 里是不合适的。 -
返回的表示,你返回的文档大部分是资源的表示,但也有一部分属于错误信息。你通过 HTTP 响应代码告诉客户端应如何对待你返回的文档。
响应代码反映了你返回的文档的用途。Content-Type 响应报头表明你返回的文档是什么格式。如果没有这个报头,客户端将无法得知如何解析或处理你返回的文档。
-
HTTP 的标准特性
-
认证与授权。基本认证,摘要认证,WSSE UsernameToken(WSSE 是 WS-Security 扩展的意思。缺点是在服务器端必须要保存密码本身),等授权过程详见本书第 238 页。
-
压缩。客户端在发送 HTTP 请求时,在 Accept-Encoding 报头里指出客户端支持哪些压缩算法。Accept-Encoding 报头有两个标准的值:compress 和 gzip。若服务器支持 Accept-Encoding 请求报头里指出的压缩算法,那么它将先对表示进行压缩,然后再发给客户端。Content-Type 响应报头的值不因服务器是否压缩而改变。但是服务器会在响应里附上一个 Content-Encoding 报头,这样客户端就知道服务器返回的表示有没有压缩了。客户端在收到服务器的响应后,先用 Content-Encoding 响应报头里指定的算法来解压缩数据,然后按 Content-Type 响应报头指定的媒体类型来处理解压缩后的数据。
-
条件 GET。条件 HTTP GET(conditional HTTP GET)既能节省服务器的带宽,又能节省客户端的带宽。条件 HTTP GET 需要由客户端和服务器共同参与完成。服务器在发送表示时,应当设置一些响应报头:Last-Modified 或/和 ETag。客户端在重复请求一个表示时,也应当设置一些报头:If-Modified-Since 或/和 If-None-Match。服务器可以根据这些信息决定是否重新发送表示。
-
缓存。条件 GET 机制令客户端可以在表示没有发生变化时,只使用很少的带宽。而缓存机制使得客户端可以根本不必发送第二次 GET 请求。
-
告诉客户端可以缓存。服务器在响应 GET 或 HEAD 请求时,可以在 Expires 响应报头里提供一个日期。告诉客户端(以及介于服务器和客户端之间的代理)应该缓存多久。若该日期是一个过去的日期,则表明响应已经过期;若该日期是一个将来的日期,则表明响应在该日期之前不会过期。到达 Expires 所指定的日期后,该响应就过期了,但这并不意味着客户端一定要将它从缓存中立即删除。客户端可以发出一个条件 GET,以确定响应是否真的过期,并更新缓存的过期时间。Expires 给出的只是一个大致的值,大部分服务器是无法预期一个响应将在何时发生改变的。假如你不想计算响应的到期日期的话,那么你可以通过 Cache-Control 来指出一个响应可以被缓存多少秒。如响应被缓存一个小时:Cache-Control: max-age=3600。
-
告诉客户端不要缓存。服务器有些响应是动态生成的,每次返回的内容都不一样。还有些事敏感信息,如果被缓存下来会造成安全问题。服务器可以通过 Cache-Control 报头告诉客户端不要对返回的表示进行缓存:Cache-Control: no-cache。Expires 报头比较简单,但是 Cache-Control 报头是相当复杂的。它是控制客户端缓存和代理缓存的主要途径。请求和响应都可以使用 Cache-Control 报头。在 Cache-Control 里指定 “max-age” 来表达一个响应在多少秒内不会过期。还可以把 Cache-Control 的值设为 “no-cache”,以禁止客户端对一个响应进行缓存。还可以把 Cache-Control 的值设为 “private”,表示响应只能被客户端缓存、不能被代理缓存。
-
默认的缓存规则。(在没有指定 Expires 和 Cache-Control 的情况下)一般情况下,客户端可以把服务器对 HTTP GET 和 HEAD 的成功响应进行缓存。这里的“成功”是由 HTTP 响应代码定义的:最常见的成功代码有 200(”OK”),301(”Moved Permanently”)和 401(”Gone”)。HTTP 标准建议:若客户端请求的 URI 里包含查询字符串,那么除非服务器明确指定可以缓存,否则服务器返回的响应不应被自动缓存;若客户端向这种 URI 发出两次 GET 请求,那么它应该引起服务器端产生两次副作用,而不是第二次得到一个缓存中的响应。假如随后有客户端向该 URI 发送 PUT,POST 或 DELETE 请求,那么所有之前为该 URI 作的缓存都立即过期。如果这个 PUT,POST 或 DELETE 请求在 Location 或 Content-Location 响应报头里设置了 URI,那么为这个 URI 作的缓存也全部立即过期。不过这里有个问题:网站 A 无法影响客户端对网站 B 的缓存策略。假如这些规则都不适用,而且服务器也没有指定一个响应可以缓存多久,那么客户端可以自己做决定:客户端可以随时把对该响应的缓存删除,也可以一直保留对该响应的缓存。
-
-
Look-Before-You-Leap 请求。条件 GET 用于防止服务器向客户端重复发送表示。HTTP 另外还有个不常被使用的特性,它可以用来防止客户端徒劳地向服务器发送巨大的(或包含敏感信息的)表示。这种请求没有一个官方的名称,于是我称它为:Look-Before-You-Leap (LBYL)请求。要发送一个 LBYL 请求,客户端像正常一样发送 PUT 或 POST 请求。但客户端并不发送实体主体,而是把 Expect 请求报头设为字符串“100-continue”。如下是一个 LBYL 请求的例子:
PUT /filestore/myfile.txt HTTP/1.1 Host: example.com Content-length: 524288000 Expect: 100-continue
这并不是一个真正的 PUT 请求。而是一个关于将要发送的 PUT 请求的询问。客户端在向服务器询问:“你允许我在 /filestore/myfile.txt 这里新建(PUT)一个表示吗?”服务器会根据客户端提供的报头及该资源的当前状态作出决定。在这里,服务器会检查 Content-length 请求报头,并决定是否愿意接受一个大小为 500MB 的文件。
如果服务器愿意,就返回响应代码 100(“Continue”)。接着,客户端可以重新发送 PUT 请求。这次请求不包含 Expect 报头了,而是在实体主体里附上那个 500MB 的表示,因为服务器已同意接受该表示了。
如果服务器不愿意接受这个表示,就返回响应代码 417(”Expectation Failed”)。服务器不愿意接受它,可能是因为 /filestore/myfile.txt 这个资源被写保护了,也可能是因为客户端没有提供正确的证书,还有可能是因为 500MB 太大了。无论什么原因,这个 look-before-you-leap 请求避免了客户端徒劳地向服务器发送 500MB 数据,服务器和客户端均因此节省了带宽。 -
部分 GET(Partial GET)。部分 HTTP GET 令客户端可以仅获取部分表示。这一特性长被用于实现断点续传。许多 Web 服务器都支持对静态内容进行部分 GET。部分 GET 看似是一种允许客户端访问给定资源的子资源的方式,但其实则不然。首选,客户端只能通过字节范围来指定一个表示的某部分。仅当你的表示是一种二进制数据结构时,这才比较有用。而且更重要的是,如果一个子资源可能会被单独引用,那么应当将它从父资源中分离出来,单独作为一个资源。一个资源是任何可以作为超链接目标的事物。所以,应该给子资源分配自己的 URI。
-
-
仿造 PUT 和 DELETE。并非所有客户端都支持 HTTP PUT 和 DELETE。但只要客户端支持 POST,客户端就可以通过“用重载的 POST(overloaded POST)来仿造 PUT 和 DELETE”的办法来绕过此限制。建议采用一种被如今许多 REST 式 Web 框架所采用的方法,即把“真正的”HTTP 方法放在查询字符串里。如 Ruby On Rails 定义了一个名为 _method 的隐藏域,用于指出“真正的” HTTP 方法。Restlet 采用 method 变量用于同样的目的。还有一种做法,即把“真正的” HTTP 动作放在 X-HTTP-Method-Override 报头里。
-
Cookies 的问题。发送 HTTP cookies 的 Web 服务是违反无状态性(statelessness)原则的。实际上它会违反无状态性原则两次:它不但把本应由客户端管理的应用状态转移到了服务器上,而且还妨碍了客户端管理自己的应用状态。Cookies 基本上是一种“服务器不加解释地强迫客户端做它所期望的事”的方式。更符合 REST 风格的方式是:服务器用超媒体链接(links)和表单(forms)来为客户端指引新的应用状态。唯一符合 REST 风格的 cookies 使用方式是:由客户端负责 cookie 的值;服务器可以通过 Set-Cookie 报头向客户端建议 cookie 的取值,但具体选择什么值,是由客户端决定的。在一些基于浏览器的应用中,客户端自己创建 cookies,而且根本不把这些 cookies 发送给服务器。这种 cookies 只是一种便利的应用状态容器,可以借助表示或 URI 把他们发送给服务器。这种做法是非常符合 REST 风格的。
-
用户凭什么信任 HTTP 客户端。在 human web 上,由于 HTTP 客户端是受我们信任的 Web 浏览器,所以没问题。但假如 HTTP 客户端是一个非受信的程序,那么把用户名、密码交给它是不安全的。还有个办法,有些 Web 服务不让客户端接触用户名、密码,以此来反钓鱼。在此种情况下,最终用户先用 Web 浏览器(绝对受信的)获取一个授权令牌(authorization token),然后把这个令牌(而不是用户名、密码)交给 Web 服务客户端,Web 服务客户端在发送请求时在 Authorization 报头里给出这个令牌。本质上,最终用户是把自己调用 Web 服务的权利委托给 Web 服务客户端来行使。若 Web 服务客户端滥用权利,用户可以收回授权令牌,而不必更换密码。