Fork me on GitHub
Bobby's Blog Hello World

《RESTful Web Services 中文版》读书笔记(二)

2019-01-27
Bobby

最近还是继续在学习 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 统一接口的一个只读子集)的资源的步骤

  1. 规划数据集

  2. 把数据集划分为资源

    记住,一个资源就是任何值得作为超链接目标的事物。任何可能用名称来引用的事物都应该有自己的名称。服务暴露的资源可以分为三类:

    • 为特别目的专门预定义的一次性资源
      这包括其他可用资源的最上层目录。大多数服务几乎不暴露一次性资源(one-off resources)。
    • 服务暴露的每一个对象做对应的资源
      一个服务可以暴露很多对象,每一种都有自己的资源集合。大多数服务暴露很多(甚至无数)个这样的资源。
    • 代表在数据集上执行算法的结果的资源
      这包括作为查询结果的集合资源。对于大多数服务,此类资源不是有无数个,就是一个也没有。

    REST 式 Web 服务通过资源(resource)来暴露数据(data)和算法(algorithms)。有关数据的资源常常构成一个层次结构(hierarchy):由很少的资源开始,然后逐渐扩展为具有许多叶节点。

  3. 用 URI 为该资源命名

    URI 能够很自然地把作用域信息组织起来。URI 设计有三条基本原则:

    1. 用路径变量(path variables)来表达层次结构(hierarchy):/parent/child。
    2. 在路径变量里加上标点符号,以消除误解:/parent/child1;child2。
      用逗号或分号表达非层次结构。建议:当作用域信息的次序重要时,就用逗号,否则就用分号。关于在 URI 里使用分号,还有一种矩阵 URI (matrix URIs),在 URI 里定义键值对。
    3. 用查询变量(query variables)来表达算法的输入,例如:/search?q=jellyfish&start=20。
  4. 设计发给客户端的表示

    表示(representation)采用的格式要能够:(1)传达资源的当前状态;(2)链接到可能的下个应用状态或资源状态。
    传达资源状态是表示的主要用途。“资源状态(resource state)”就是有关资源的任何信息。
    表示的另一个用途是推进状态。一个资源的表示应该链接到邻近的资源,比如可能的下个应用状态(application state)。这样做是为了实现连通性(connectedness)——通过跟随链接(following links)从一个资源到另一个资源的能力。

  5. 用超链接和表单把该资源与已有资源联系起来

    客户端搜索的结果可能有无数种,但是我们不可能为所有搜索结果都专门列出一个链接。HTML 通过表单(Form)来解决这种问题。我们可以通过在表示里提供一个表单,告诉客户端如何在查询字符串(query string)里带入变量。该表单代表着无数个遵循一定模式的 URIs。

  6. 考虑有哪些典型的事件经过

    许多只读资源的典型事件经过(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)实现的。

  7. 考虑可能出现哪些错误情况

    • 如果不存在,响应代码 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 基本认证时这点尤为重要,因为在这种认证机制下,客户端是通过纯文本发送证书的。

创建读写资源的步骤

  1. 规划数据集

  2. 把数据集划分为资源

  3. 用 URI 为该资源命名

  4. 暴露一个统一接口的子集

    可以通过回答以下问题来决定暴露统一接口的哪个子集:

    • 客户端将创建这种新资源吗?
    • 当客户端创建这种新资源时,谁负责决定这个新资源的 URI?客户端还是服务器?
    • 客户端将修改这种资源吗?
    • 客户端将删除这种资源吗?
    • 客户端将获取这种资源的表示吗?
  5. 设计来自客户端的表示

    表单编码

    这种表示没有官方的名称,它只有媒体类型(application/x-www-form-urlencoded)。它有时被称为“CGI 转义(CGI escaping)”。当你通过 Web 浏览器提交 HTML 表单时,浏览器就是用这种格式把表单数据编组(marshal)为“可以放在 HTTP 请求里的格式”的。
    当一个对象的状态可以表达为键-值对(key-value pairs)时,表达编码(form-encoding)是最简单的表示格式。差不多所有编程语言都为表单编码及解码提供内在支持。

  6. 设计发给客户端的表示

  7. 用超链接和表单把该资源与已有资源联系起来

  8. 考虑有哪些典型的时间经过

    若创建成功,返回响应代码 201(”Created”),Location 报头里包含新创建的资源的 URI。
    若修改成功,返回响应代码 200(”OK”),并在响应主体里包含资源的表示。若修改不影响资源的表示,也可以把响应代码设为 205 (”Reset Content”),并省略响应实体主体。若修改导致了资源的 URI 改变,那么应该返回响应代码 301(”Moved Permanently”),并在 Location 报头里给出新的 URI。
    删除一个资源后,服务器应该返回代码 200(”OK”)。

  9. 考虑可能出现哪些错误情况

    服务端无法理解客户端的表示时,响应代码是 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 服务客户端滥用权利,用户可以收回授权令牌,而不必更换密码。


下一篇 Java 9 新特性

Comments

Content