RESTful服务最佳实践

本文主要读者html

引言前端

REST是什么git

  统一接口github

    基于资源web

    经过表征来操做资源ajax

    自描述的信息正则表达式

    超媒体即应用状态引擎(HATEOAS)数据库

  无状态编程

  可缓存json

  C-S架构

  分层系统

  按需编码(可选)

REST快速提示

  使用HTTP动词表示一些含义

  合理的资源名

  XML和JSON

  建立适当粒度的资源

  考虑连通性

定义

  幂等性

  安全

HTTP动词

  GET

  PUT

  POST

  PUT和POST的建立比较

  DELETE

资源命名

  资源URI示例

  资源命名的反例

  复数

返回表征

  资源经过连接的可发现性(HATEOAS续)

    最小化连接推荐

    连接格式

  封装响应

  处理跨域问题

    支持CORS

    支持JSONP

查询,过滤和分页

  结果限制

    用范围标记进行限制

    用字符串查询参数进行限制

    基于范围的响应

  分页

  结果的过滤和排序

    过滤

    排序

服务版本管理

  经过内容协商支持版本管理

  当没有指定版本时,返回什么版本?

  请求不支持的版本

  何时应该建立一个新版本?

    破坏性的修改

    非破坏性的修改

  版本控制应在什么级别出现?

  利用Content-Location来加强响应

  带有Content-Type的连接

  找出支持的版本

    我应该同时支持多少个版本?

    弃用

    我如何告知客户端被弃用的资源?

日期/时间处理

  Body内容中的日期/时间序列化

  HTTP Headers中的日期/时间序列化

保护服务的安全

  身份验证

  传输安全

  受权

  应用程序安全

缓存和可伸缩性

  ETag Header

HTTP状态码(前10)

附加资源

  书籍

  网站

 

本文主要读者

  该最佳实践文档适用于对RESTful Web服务感兴趣的开发人员,该服务为跨多个服务的组件提供了较高的可靠性和一致性。按照本文的指导,可快速、普遍、公开地为内外部客户采用。

  本文中的指导原则一样适用于工程师们,他们但愿使用这些依据最佳实践原则开发的服务。虽然他们更加关注缓存、代理规则、监听及安全等相关方面,可是该文档能做为一份涵盖全部种类服务的总指南。

  另外,经过从这些指导原则,管理人员了解到建立公共的、提供高稳定性的服务所需花费的努力,他们也可从中受益。

 

引言

  现今已有大量关于RESTful Web服务最佳实践的相关资料(详见本文最后的相关文献部分)。因为创做的时间不一样,许多资料中的内容是矛盾的。此外,想要经过查阅文献来了解这种服务的发展是不太可取的。为了了解RESTful这一律念,至少须要查阅三到五本相关文献,而本文将可以帮你加速这一过程——摒弃多余的讨论,最大化地提炼出REST的最佳实践和规范。

  与其说REST是一套标准,REST更像是一种原则的集合。除了六个重要的原则外就没有其余的标准了。实际上,虽然有所谓的“最佳实践”和标准,但这些东西都和宗教斗争同样,在不断地演化。

  本文围绕REST的广泛问题提出了意见和仿食谱式的讨论,并经过介绍一些简单的背景知识对建立真实情境下的预生产环境中一致的REST服务提供知识。本文收集了来自其余渠道的信息,经历过一次次的失败后不断改进。

  但对于REST模式是否必定比SOAP好用仍有较大争议(反之亦然),也许在某些状况下仍须要建立SOAP服务。本文在说起SOAP时并未花较大篇幅来讨论它的相对优势。相反因为技术和行业在不断进步,咱们将继续坚持咱们的假设–REST是当下设计web服务的最佳方法。

  第一部分概述REST的含义、设计准则和它的独特之处。第二部分列举了一些小贴士来记忆REST的服务理念。以后的部分则会更深刻地为web服务建立人员提供一些细节的支持和讨论,来实现一个可以公开展现在生产环境中的高质量REST服务。

 

REST是什么?

  REST架构方式描述了六种设计准则。这些用于架构的设计准则,最先是由Roy Fielding在他的博士论文中提出并定义了RESTful风格。(详见http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm

  六个设计准则分别是:

  • 统一接口
  • 无状态
  • 可缓冲
  • C-S架构
  • 分层系统
  • 按需编码

  如下是这些设计准则的详细讨论:

统一接口

  统一接口准则定义了客户端和服务端之间的接口,简化和分离了框架结构,这样一来每一个部分均可独立演化。如下是接口统一的四个原则:

  基于资源

  不一样资源须要用URI来惟一标识。返回给客户端的表征和资源自己在概念上有所不一样,例如服务端不会直接传送一个数据库资源,然而,一些HTML、XML或JSON数据可以展现部分数据库记录,如用芬兰语来表述仍是用UTF-8编码则要根据请求和服务器实现的细节来决定。

  经过表征来操做资源

  当客户端收到包含元数据的资源的表征时,在有权限的状况下,客户端已掌握的足够的信息,能够对服务端的资源进行删改。

  自描述的信息

  每条信息都包含足够的数据用以确认信息该如何处理。例如要由网络媒体类型(已知的如MIME类型)来确认需调用哪一个解析器。响应一样也代表了它们的缓存能力。

  超媒体即应用状态引擎(HATEOAS)

  客户端经过body内容、查询串参数、请求头和URI(资源名称)来传送状态。服务端经过body内容,响应码和响应头传送状态给客户端。这项技术被称为超媒体(或超文本连接)。

  除了上述内容外,HATEOS也意味着,必要的时候连接也可被包含在返回的body(或头部)中,以提供URI来检索对象自己或关联对象。下文将对此进行更详细的阐述。

  统一接口是每一个REST服务设计时的必要准则。

无状态

  正如REST是REpresentational State Transfer的缩写,无状态很关键。本质上,这代表了处理请求所需的状态已经包含在请求自己里,也有多是URI的一部分、查询串参数、body或头部。URI可以惟一标识每一个资源,body中也包含了资源的转态(或转态变动状况)。以后,服务器将进行处理,将相关的状态或资源经过头部、状态和响应body传递给客户端。

  从事咱们这一行业的大多数人都习惯使用容器来编程,容器中有一个“会话”的概念,用于在多个HTTP请求下保持状态。在REST中,若是要在多个请求下保持用户状态,客户端必须囊括客户端的全部信息来完成请求,必要时从新发送请求。自从服务端不须要维持、更新或传递会话状态后,无状态性获得了更大的延展。此外,负载均衡器无需担忧和无状态系统之间的会话。

  因此状态和资源间有什么差异?服务器对于状态,或者说是应用状态,所关注的点是在当前会话或请求中要完成请求所需的数据。而资源,或者说是资源状态,则是定义了资源表征的数据,例如存储在数据库中的数据。因而可知,应用状态是是随着客户端和请求的改变而改变的数据。相反,资源状态对于发出请求的客户端来讲是不变的。

  在网络应用的某一特定位置上摆放一个返回按钮,是由于它但愿你能按必定的顺序来操做吗?实际上是由于它违反了无状态的原则。有许多不遵照无状态原则的案例,例如3-Legged OAuth,API调用速度限制等。但仍是要尽可能确保服务器中不须要在多个请求下保持应用状态。

可缓存

  在万维网上,客户端能够缓存页面的响应内容。所以响应都应隐式或显式的定义为可缓存的,若不可缓存则要避免客户端在屡次请求后用旧数据或脏数据来响应。管理得当的缓存会部分地或彻底地除去客户端和服务端之间的交互,进一步改善性能和延展性。

C-S架构

  统一接口使得客户端和服务端相互分离。关注分离意味什么?打个比方,客户端不须要存储数据,数据都留在服务端内部,这样使得客户端代码的可移植性获得了提高;而服务端不须要考虑用户接口和用户状态,这样一来服务端将更加简单易拓展。只要接口不改变,服务端和客户端能够单独地进行研发和替换。

分层系统

  客户端一般没法代表本身是直接仍是间接与端服务器进行链接。中介服务器能够经过启用负载均衡或提供共享缓存来提高系统的延展性。分层时一样要考虑安全策略。

按需编码(可选)

  服务端经过传输可执行逻辑给客户端,从而为其临时拓展和定制功能。相关的例子有编译组件Java applets和客户端脚本JavaScript。

  听从上述原则,与REST架构风格保持一致,能让各类分布式超媒体系统拥有指望的天然属性,好比高性能,延展性,简洁,可变性,可视化,可移植性和可靠性。

  提示:REST架构中的设计准则中,只有按需编码为可选项。若是某个服务违反了其余任意一项准则,严格意思上不能称之为RESTful风格。

 

REST快速提示

  (根据上面提到的六个原则)无论在技术上是否是RESTful的,这里有一些相似REST概念的建议。遵循它们,能够实现更好、更有用的服务:

使用HTTP动词表示一些含义

  任何API的使用者可以发送GET、POST、PUT和DELETE请求,它们很大程度明确了所给请求的目的。同时,GET请求不能改变任何潜在的资源数据。测量和跟踪仍可能发生,但只会更新数据而不会更新由URI标识的资源数据。

合理的资源名

  合理的资源名称或者路径(如/posts/23而不是/api?type=posts&id=23)能够更明确一个请求的目的。使用URL查询串来过滤数据是很好的方式,但不该该用于定位资源名称。

  适当的资源名称为服务端请求提供上下文,增长服务端API的可理解性。经过URI名称分层地查看资源,能够给使用者提供一个友好的、容易理解的资源层次,以在他们的应用程序上应用。资源名称应该是名词,避免为动词。使用HTTP方法来指定请求的动做部分,能让事情更加的清晰。

XML和JSON

  建议默认支持json,而且,除非花费很惊人,不然就同时支持json和xml。在理想状况下,让使用者仅经过改变扩展名.xml和.json来切换类型。此外,对于支持ajax风格的用户界面,一个被封装的响应是很是有帮助的。提供一个被封装的响应,在默认的或者有单独扩展名的状况下,例如:.wjson和.wxml,代表客户端请求一个被封装的json或xml响应(请参见下面的封装响应)。

  “标准”中对json的要求不多。而且这些需求只是语法性质的,无关内容格式和布局。换句话说,REST服务端调用的json响应是协议的一部分——在标准中没有相关描述。更多关于json数据格式能够在http://www.json.org/上找到。

  关于REST服务中xml的使用,xml的标准和约定除了使用语法正确的标签和文本外没有其它的做用。特别地,命名空间不是也不该该是被使用在REST服务端的上下文中。xml的返回更相似于json——简单、容易阅读,没有模式和命名空间的细节呈现——仅仅是数据和连接。若是它比这更复杂的话,参看本节的第一段——使用xml的成本是惊人的。鉴于咱们的经验,不多有人使用xml做为响应。在它被彻底淘汰以前,这是最后一个可被确定的地方。

建立适当粒度的资源

  一开始,系统中模拟底层应用程序域或数据库架构的API更容易被建立。最终,你会但愿将这些服务都整合到一块儿——利用多项底层资源减小通讯量。在建立独立的资源以后再建立更大粒度的资源,比从更大的合集中建立较大粒度的资源更加容易一些。从一些小的容易定义的资源开始,建立CRUD(增删查改)功能,能够使资源的建立变得更容易。随后,你能够建立这些基于用例和减小通讯量的资源。

考虑连通性

  REST的原理之一就是连通性——经过超媒体连接实现。当在响应中返回连接时,api变的更具备自描述性,而在没有它们时服务端依然可用。至少,接口自己能够为客户端提供如何检索数据的参考。此外,在经过POST方法建立资源时,还能够利用头位置包含一个连接。对于响应中支持分页的集合,"first"、 "last"、"next"、和"prev"连接至少是很是有用的。

 

定义

幂等性

  不要从字面意思来理解什么是幂等性,偏偏相反,这与某些功能紊乱的领域无关。下面是来自维基百科的解释:

在计算机科学中,术语幂等用于更全面地描述一个操做,一次或屡次执行该操做产生的结果是一致的。根据应用的上下文,这可能有不一样的含义。例如,在方法或者子例程调用具备反作用的状况下,意味着在第一调用以后被修改的状态也保持不变。

  从REST服务端的角度来看,因为操做(或服务端调用)是幂等的,客户端能够用重复的调用而产生相同的结果——在编程语言中操做像是一个"setter"(设置)方法。换句话说,就是使用多个相同的请求与使用单个请求效果相同。注意,当幂等操做在服务器上产生相同的结果(反作用),响应自己多是不一样的(例如在多个请求之间,资源的状态可能会改变)。

  PUT和DELETE方法被定义为是幂等的。查看http请求中delete动词的警告信息,能够参照下文的DELETE部分。GET、HEAD、OPTIO和TRACE方法自从被定义为安全的方法后,也被定义为幂等的。参照下面关于安全的段落。

安全

  来自维基百科:

一些方法(例如GET、HEAD、OPTIONS和TRACE)被定义为安全的方法,这意味着它们仅被用于信息检索,而不能更改服务器的状态。换句话说,它们不会有反作用,除了相对来讲无害的影响如日志、缓存、横幅广告或计数服务等。任意的GET请求,不考虑应用状态的上下文,都被认为是安全的。

  总之,安全意味着调用的方法不会引发反作用。所以,客户端能够反复使用安全的请求而不用担忧对服务端产生任何反作用。这意味着服务端必须遵照GET、HEAD、OPTIONS和TRACE操做的安全定义。不然,除了对消费端产生混淆外,它还会致使Web缓存,搜索引擎以及其它自动代理的问题——这将在服务器上产生意想不到的后果。

  根据定义,安全操做是幂等的,由于它们在服务器上产生相同的结果。

  安全的方法被实现为只读操做。然而,安全并不意味着服务器必须每次都返回相同的响应。

 

HTTP动词

  Http动词主要遵循“统一接口”规则,并提供给咱们对应的基于名词的资源的动做。最主要或者最经常使用的http动词(或者称之为方法,这样称呼可能更恰当些)有POST、GET、PUT和DELETE。这些分别对应于建立、读取、更新和删除(CRUD)操做。也有许多其它的动词,可是使用频率比较低。在这些使用较少的方法中,OPTIONS和HEAD每每使用得更多。

GET

  HTTP的GET方法用于检索(或读取)资源的数据。在正确的请求路径下,GET方法会返回一个xml或者json格式的数据,以及一个200的HTTP响应代码(表示正确返回结果)。在错误状况下,它一般返回404(不存在)或400(错误的请求)。

  例如:

  GET http://www.example.com/customers/12345
  GET http://www.example.com/customers/12345/orders
  GET http://www.example.com/buckets/sample

  按照HTTP的设计规范,GET(以及附带的HEAD)请求仅用于读取数据而不改变数据。所以,这种使用方式被认为是安全的。也就是说,它们的调用没有数据修改或污染的风险——调用1次和调用10次或者没有被调用的效果同样。此外,GET(以及HEAD)是幂等的,这意味着使用多个相同的请求与使用单个的请求最终都拥有相同的结果。

  不要经过GET暴露不安全的操做——它应该永远都不能修改服务器上的任何资源。

PUT

  PUT一般被用于更新资源。经过PUT请求一个已知的资源URI时,须要在请求的body中包含对原始资源的更新数据。

  不过,在资源ID是由客服端而非服务端提供的状况下,PUT一样能够被用来建立资源。换句话说,若是PUT请求的URI中包含的资源ID值在服务器上不存在,则用于建立资源。同时请求的body中必须包含要建立的资源的数据。有人以为这会产生歧义,因此除非真的须要,使用这种方法来建立资源应该被慎用。

  或者咱们也能够在body中提供由客户端定义的资源ID而后使用POST来建立新的资源——假设请求的URI中不包含要建立的资源ID(参见下面POST的部分)。

  例如:

  PUT http://www.example.com/customers/12345
  PUT http://www.example.com/customers/12345/orders/98765
  PUT http://www.example.com/buckets/secret_stuff

  当使用PUT操做更新成功时,会返回200(或者返回204,表示返回的body中不包含任何内容)。若是使用PUT请求建立资源,成功返回的HTTP状态码是201。响应的body是可选的——若是提供的话将会消耗更多的带宽。在建立资源时没有必要经过头部的位置返回连接,由于客户端已经设置了资源ID。请参见下面的返回值部分。

  PUT不是一个安全的操做,由于它会修改(或建立)服务器上的状态,但它是幂等的。换句话说,若是你使用PUT建立或者更新资源,而后重复调用,资源仍然存在而且状态不会发生变化。

  例如,若是在资源增量计数器中调用PUT,那么这个调用方法就再也不是幂等的。这种状况有时候会发生,且可能足以证实它是非幂等性的。不过,建议保持PUT请求的幂等性。并强烈建议非幂等性的请求使用POST。

POST

  POST请求常常被用于建立新的资源,特别是被用来建立从属资源。从属资源即归属于其它资源(如父资源)的资源。换句话说,当建立一个新资源时,POST请求发送给父资源,服务端负责将新资源与父资源进行关联,并分配一个ID(新资源的URI),等等。

  例如:

  POST http://www.example.com/customers
  POST http://www.example.com/customers/12345/orders

  当建立成功时,返回HTTP状态码201,并附带一个位置头信息,其中带有指向最早建立的资源的连接。

  POST请求既不是安全的又不是幂等的,所以它被定义为非幂等性资源请求。使用两个相同的POST请求极可能会致使建立两个包含相同信息的资源。

PUT和POST的建立比较

  总之,咱们建议使用POST来建立资源。当由客户端来决定新资源具备哪些URI(经过资源名称或ID)时,使用PUT:即若是客户端知道URI(或资源ID)是什么,则对该URI使用PUT请求。不然,当由服务器或服务端来决定建立的资源的URI时则使用POST请求。换句话说,当客户端在建立以前不知道(或没法知道)结果的URI时,使用POST请求来建立新的资源。

DELETE

  DELETE很容易理解。它被用来根据URI标识删除资源。

  例如:

  DELETE http://www.example.com/customers/12345
  DELETE http://www.example.com/customers/12345/orders
  DELETE http://www.example.com/buckets/sample

  当删除成功时,返回HTTP状态码200(表示正确),同时会附带一个响应体body,body中可能包含了删除项的数据(这会占用一些网络带宽),或者封装的响应(参见下面的返回值)。也能够返回HTTP状态码204(表示无内容)表示没有响应体。总之,能够返回状态码204表示没有响应体,或者返回状态码200同时附带JSON风格的响应体。

  根据HTTP规范,DELETE操做是幂等的。若是你对一个资源进行DELETE操做,资源就被移除了。在资源上反复调用DELETE最终致使的结果都相同:即资源被移除了。但若是将DELETE的操做用于计数器(资源内部),则DETELE将再也不是幂等的。如前面所述,只要数据没有被更新,统计和测量的用法依然可被认为是幂等的。建议非幂等性的资源请求使用POST操做。

  然而,这里有一个关于DELETE幂等性的警告。在一个资源上第二次调用DELETE每每会返回404(未找到),由于该资源已经被移除了,因此找不到了。这使得DELETE操做再也不是幂等的。若是资源是从数据库中删除而不是被简单地标记为删除,这种状况须要适当妥协。

  下表总结出了主要HTTP的方法和资源URI,以及推荐的返回值:

HTTP请求 /customers /customers/{id}
GET 200(正确),用户列表。使用分页、排序和过滤大导航列表。 200(正确),查找单个用户。若是ID没有找到或ID无效则返回404(未找到)。
PUT 404(未找到),除非你想在整个集合中更新/替换每一个资源。 200(正确)或204(无内容)。若是没有找到ID或ID无效则返回404(未找到)。
POST 201(建立),带有连接到/customers/{id}的位置头信息,包含新的ID。 404(未找到)
DELETE 404(未找到),除非你想删除整个集合——一般不被容许。 200(正确)。若是没有找到ID或ID无效则返回404(未找到)。

 

资源命名

  除了适当地使用HTTP动词,在建立一个能够理解的、易于使用的Web服务API时,资源命名能够说是最具备争议和最重要的概念。一个好的资源命名,它所对应的API看起来更直观而且易于使用。相反,若是命名很差,一样的API会让人感受很笨拙而且难以理解和使用。当你须要为你的新API建立资源URL时,这里有一些小技巧值得借鉴。

  从本质上讲,一个RESTFul API最终均可以被简单地看做是一堆URI的集合,HTTP调用这些URI以及一些用JSON和(或)XML表示的资源,它们中有许多包含了相互关联的连接。RESTful的可寻址能力主要依靠URI。每一个资源都有本身的地址或URI——服务器能提供的每个有用的信息均可以做为资源来公开。统一接口的原则部分地经过URI和HTTP动词的组合来解决,并符合使用标准和约定。

  在决定你系统中要使用的资源时,使用名词来命名这些资源,而不是用动词或动做来命名。换句话说,一个RESTful URI应该关联到一个具体的资源,而不是关联到一个动做。另外,名词还具备一些动词没有的属性,这也是另外一个显著的因素。

  一些资源的例子:

  • 系统的用户
  • 学生登记的课程
  • 一个用户帖子的时间轴
  • 关注其余用户的用户
  • 一篇关于骑马的文章

  服务套件中的每一个资源至少有一个URI来标识。若是这个URI能表示必定的含义而且可以充分描述它所表明的资源,那么它就是一个最好的命名。URI应该具有可预测性和分层结构,这将有助于提升它们的可理解性和可用性的:可预测指的是资源应该和名称保持一致;而分层指的是数据具备关系上的结构。这并不是REST规则或规范,可是它强化了对API的定义。

  RESTful API是提供给消费端的。URI的名称和结构应该将它所表达的含义传达给消费者。一般咱们很难知道数据的边界是什么,可是从你的数据上你应该颇有可能去尝试找到要返回给客户端的数据是什么。API是为客户端而设计的,而不是为你的数据。

  假设咱们如今要描述一个包括客户、订单,列表项,产品等功能的订单系统。考虑一下咱们该如何来描述在这个服务中所涉及到的资源的URIs:

资源URI示例

  为了在系统中插入(建立)一个新的用户,咱们能够使用:

  POST http://www.example.com/customers

 

  读取编号为33245的用户信息:

  GET http://www.example.com/customers/33245

  使用PUT和DELETE来请求相同的URI,能够更新和删除数据。

 

  下面是对产品相关的URI的一些建议:

  POST http://www.example.com/products

  用于建立新的产品。

 

  GET|PUT|DELETE http://www.example.com/products/66432

  分别用于读取、更新、删除编号为66432的产品。

 

  那么,如何为用户建立一个新的订单呢?

  一种方案是:

  POST http://www.example.com/orders

  这种方式能够用来建立订单,但缺乏相应的用户数据。

  

  由于咱们想为用户建立一个订单(注意之间的关系),这个URI可能不够直观,下面这个URI则更清晰一些:

  POST http://www.example.com/customers/33245/orders

  如今咱们知道它是为编号33245的用户建立一个订单。

 

  那下面这个请求返回的是什么呢?

  GET http://www.example.com/customers/33245/orders

  多是一个编号为33245的用户所建立或拥有的订单列表。注意:咱们能够屏蔽对该URI进行DELETE或PUT请求,由于它的操做对象是一个集合。

 

  继续深刻,那下面这个URI的请求又表明什么呢?

  POST http://www.example.com/customers/33245/orders/8769/lineitems

  多是(为编号33245的用户)增长一个编号为8769的订单条目。没错!若是使用GET方式请求这个URI,则会返回这个订单的全部条目。可是,若是这些条目与用户信息无关,咱们将会提供POST www.example.com/orders/8769/lineitems这个URI。

  从返回的这些条目来看,指定的资源可能会有多个URIs,因此咱们可能也须要要提供这样一个URI GET http://www.example.com/orders/8769,用来在不知道用户ID的状况下根据订单ID来查询订单。

 

  更进一步:

  GET http://www.example.com/customers/33245/orders/8769/lineitems/1

  可能只返回同个订单中的第一个条目。

  如今你应该理解什么是分层结构了。它们并非严格的规则,只是为了确保在你的服务中这些强制的结构可以更容易被用户所理解。与全部软件开发中的技能同样,命名是成功的关键。

  

  多看一些API的示例并学会掌握这些技巧,和你的队友一块儿来完善你API资源的URIs。这里有一些APIs的例子:

资源命名的反例

  前面咱们已经讨论过一些恰当的资源命名的例子,然而有时一些反面的例子也颇有教育意义。下面是一些不太具备RESTful风格的资源URIs,看起来比较混乱。这些都是错误的例子! 

  首先,一些serivices每每使用单一的URI来指定服务接口,而后经过查询参数来指定HTTP请求的动做。例如,要更新编号12345的用户信息,带有JSON body的请求多是这样:

  GET http://api.example.com/services?op=update_customer&id=12345&format=json

  尽管上面URL中的"services"的这个节点是一个名词,但这个URL不是自解释的,由于对于全部的请求而言,该URI的层级结构都是同样的。此外,它使用GET做为HTTP动词来执行一个更新操做,这简直就是反人类(甚至是危险的)。

  下面是另一个更新用户的操做的例子:

  GET http://api.example.com/update_customer/12345

  以及它的一个变种:

  GET http://api.example.com/customers/12345/update

  你会常常看到在其余开发者的服务套件中有不少这样的用法。能够看出,这些开发者试图去建立RESTful的资源名称,并且已经有了一些进步。可是你仍然可以识别出URL中的动词短语。注意,在这个URL中咱们不须要"update"这个词,由于咱们能够依靠HTTP动词来完成操做。下面这个URL正好说明了这一点:

  PUT http://api.example.com/customers/12345/update

  这个请求同时存在PUT和"update",这会对消费者产生迷惑!这里的"update"指的是一个资源吗?所以,这里咱们费些口舌也是但愿你可以明白……

复数

  让咱们来讨论一下复数和“单数”的争议…还没据说过?但这种争议确实存在,事实上它能够归结为这个问题……

  在你的层级结构中URI节点是否须要被命名为单数或复数形式呢?举个例子,你用来检索用户资源的URI的命名是否须要像下面这样:

  GET http://www.example.com/customer/33245

  或者:

  GET http://www.example.com/customers/33245

  两种方式都没问题,但一般咱们都会选择使用复数命名,以使得你的API URI在全部的HTTP方法中保持一致。缘由是基于这样一种考虑:customers是服务套件中的一个集合,而ID33245的这个用户则是这个集合中的其中一个。

  按照这个规则,一个使用复数形式的多节点的URI会是这样(注意粗体部分):

  GET http://www.example.com/customers/33245/orders/8769/lineitems/1

  “customers”、“orders”以及“lineitems”这些URI节点都使用的是复数形式。

  这意味着你的每一个根资源只须要两个基本的URL就能够了,一个用于建立集合内的资源,另外一个用来根据标识符获取、更新和删除资源。例如,以customers为例,建立资源能够使用下面的URL进行操做:

  POST http://www.example.com/customers

  而读取、更新和删除资源,使用下面的URL操做:

  GET|PUT|DELETE http://www.example.com/customers/{id}

  正如前面提到的,给定的资源可能有多个URI,但做为一个最小的完整的增删改查功能,利用两个简单的URI来处理就够了。

  或许你会问:是否在有些状况下复数没有意义?嗯,事实上是这样的。当没有集合概念的时候(此时复数没有意义)。换句话说,当资源只有一个的状况下,使用单数资源名称也是能够的——即一个单一的资源。例如,若是有一个单一的整体配置资源,你能够使用一个单数名称来表示:

  GET|PUT|DELETE http://www.example.com/configuration

  注意这里缺乏configuration的ID以及HTTP动词POST的用法。假设每一个用户有一个配置的话,那么这个URL会是这样:

  GET|PUT|DELETE http://www.example.com/customers/12345/configuration

  一样注意这里没有指定configuration的ID,以及没有给定POST动词的用法。在这两个例子中,可能也会有人认为使用POST是有效的。好吧...

 

返回表征

  正如前面提到的,RESTful接口支持多种资源表征,包括JSON和XML,以及被封装的JSON和XML。建议JSON做为默认表征,不过服务端应该容许客户端指定其它表征。

  对于客户端请求的表征格式,咱们能够在Accept头经过文件扩展名来进行指定,也能够经过query-string等其它方式来指定。理想状况下,服务端能够支持全部这些方法。可是,如今业内更倾向于经过相似于文件扩展名的方式来进行指定。所以,建议服务端至少须要支持使用文件扩展名的方式,例如“.json”,“.xml”以及它们的封装版本“.wjon”,“.wxml”。

  经过这种方式,在URI中指定返回表征的格式,能够提升URL的可见性。例如,GET http://www.example.com/customers.xml将返回customer列表的XML格式的表征。一样,GET http://www.example.com/customers.json将返回一个JSON格式的表征。这样,即便是在最基础的客户端(例如“curl”),服务使用起来也会更加简便。推荐使用这种方式。

  此外,当url中没有包含格式说明时,服务端应该返回默认格式的表征(假设为JSON)。例如:

  GET http://www.example.com/customers/12345

  GET http://www.example.com/customers/12345.json

  以上二者返回的ID为12345的customer数据均为JSON格式,这是服务端的默认格式。

  GET http://www.example.com/customers/12345.xml

  若是服务端支持的话,以上请求返回的ID为12345的customer数据为XML格式。若是该服务器不支持XML格式的资源,将返回一个HTTP 404的错误。

  使用HTTP Accept头被普遍认为是一种更优雅的方式,而且符合HTTP的规范和含义,客户端能够经过这种方式来告知HTTP服务端它们可支持的数据类型有哪些。可是,为了使用Accept头,服务端要同时支持封装和未封装的响应,你必须实现自定义的类型——由于这些格式不是标准的类型。这大大增长了客户端和服务端的复杂性。请参见RFC 2616的14.1节有关Accept头的详细信息(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1)。使用文件扩展名来指定数据格式是最简单直接的方法,用最少的字符就能够完成,而且支持脚本操做——无需利用HTTP头。

  一般当咱们提到REST服务,跟XML是绝不相关的。即便服务端支持XML,也几乎没有人建议在REST中使用XML。XML的标准和公约在REST中不太适用。特别是它连命名空间都没有,就更不应在RESTful服务体系中使用了。这只会使事情变得更复杂。因此返回的XML看起来更像JSON,它简单易读,没有模式和命名空间的限制,换句话来讲是无标准的,易于解析。

资源经过连接的可发现性(HATEOAS续)

  REST指导原则之一(根据统一接口原则)是application的状态经过hypertext(超文本)来传输。这就是咱们一般所说的Hypertext As The Engine of Application State (即HATEOAS,用超文原本做为应用程序状态机),咱们在“REST是什么”一节中也提到过。

  根据Roy Fielding在他的博客中的描述(http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertextdriven),REST接口中最重要的部分是超文本的使用。此外,他还指出,在给出任何相关的信息以前,一个API应该是可用和可理解的。也就是说,一个API应当能够经过其连接导航到数据的各个部分。不建议只返回纯数据。

  不过目前的业界先驱们并无常常采用这种作法,这反映了HATEOAS仅仅在成熟度模型中的使用率更高。纵观众多的服务体系,它们大多返回更多的数据,而返回的连接却不多(或者没有)。这是违背Fielding的REST约定的。Fielding说:“信息的每个可寻址单元都携带一个地址……查询结果应该表现为一个带有摘要信息的连接清单,而不是对象数组。”

  另外一方面,简单粗暴地将整个连接集合返回会大大影响网络带宽。在实际状况中,根据所需的条件或使用状况,API接口的通讯量要根据服务器响应中超文本连接所包含的“摘要”数量来平衡。

  同时,充分利用HATEOAS可能会增长实现的复杂性,并对服务客户端产生明显的负担,这至关于下降了客户端和服务器端开发人员的生产力。所以,当务之急是要平衡超连接服务实践和现有可用资源之间的问题。

  超连接最小化的作法是在最大限度地减小客户端和服务器之间的耦合的同时,提升服务端的可用性、可操纵性和可理解性。这些最小化建议是:经过POST建立资源并从GET请求返回集合,对于有分页的状况后面咱们会提到。

最小化连接推荐

  在create的用例中,新建资源的URI(连接)应该在Location响应头中返回,且响应主体是空的——或者只包含新建资源的ID。

  对于从服务端返回的表征集合,每一个表征应该在它的连接集合中携带一个最小的“自身”连接属性。为了方便分页操做,其它的连接能够放在一个单独的连接集合中返回,必要时能够带有“第一页”、“上一页”、“下一页”、“最后一页”等信息。

  参照下文连接格式部分的例子获取更多信息。

连接格式

  参照整个连接格式的标准,建议遵照一些相似Atom、AtomPub或Xlink的风格。JSON-LD也不错,但并无被普遍采用(若是曾经被用过)。目前业内最广泛的方式是使用带有"rel"元素和包含资源完整URI的"href"元素的Atom连接格式,不包含任何身份验证或查询字符串参数。"rel"元素能够包含标准值"alternate"、"related"、"self"、"enclosure"和"via",还有分页连接的“第一页”、“上一页”、“下一页”,“最后一页”。在须要时能够自定义并添加使用它们。

  一些XML Atom格式的概念对于用JSON格式表示的连接来讲是无用的。例如,METHOD属性对于一个RESTful资源来讲是不须要的,由于对于一个给定的资源,在全部支持的HTTP方法(CRUD行为)中,资源的URI都是相同的——因此单独列出这些是没有必要的。

  让咱们举一些具体的例子来进一步说明这一点。下面是调用建立新资源的请求后的响应:

  POST http://api.example.com/users

  下面是响应头集合中带有建立新资源的URI的Location部分:

HTTP/1.1 201 CREATED 
Status: 201 
Connection: close 
Content-Type: application/json; charset=utf-8 
Location: http://api.example.com/users/12346

  返回的body能够为空,或者包含一个被封装的响应(见下文封装响应)。

  下面的例子经过GET请求获取一个不包含分页的表征集合的JSON响应:

{
  "data": [
    {
      "user_id": "42",
      "name": "Bob",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/42"
        }
      ]
    },
    {
      "user_id": "22",
      "name": "Frank",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/22"
        }
      ]
    },
    {
      "user_id": "125",
      "name": "Sally",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/125"
        }
      ]
    }
  ]
}

  注意,links数组中的每一项都包含一个指向“自身(self)”的连接。该数组还可能还包含其它关系,如children、parent等。

  最后一个例子是经过GET请求获取一个包含分页的表征集合的JSON响应(每页显示3项),咱们给出第三页的数据:

{
  "data": [
    {
      "user_id": "42",
      "name": "Bob",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/42"
        }
      ]
    },
    {
      "user_id": "22",
      "name": "Frank",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/22"
        }
      ]
    },
    {
      "user_id": "125",
      "name": "Sally",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/125"
        }
      ]
    }
  ],
  "links": [
    {
      "rel": "first",
      "href": "http://api.example.com/users?offset=0&limit=3"
    },
    {
      "rel": "last",
      "href": "http://api.example.com/users?offset=55&limit=3"
    },
    {
      "rel": "previous",
      "href": "http://api.example.com/users?offset=3&limit=3"
    },
    {
      "rel": "next",
      "href": "http://api.example.com/users?offset=9&limit=3"
    }
  ]
}

  在这个例子中,响应中用于分页的links集合中的每一项都包含一个指向“自身(self)”的连接。这里可能还会有一些关联到集合的其它连接,但都与分页自己无关。简而言之,这里有两个地方包含links。一个就是data对象中所包含的集合(这个也是接口要返回给客户端的数据表征集合),其中的每一项至少要包括一个指向“自身(self)”的links集合;另外一个则是一个单独的对象links,其中包括和分页相关的连接,该部分的内容适用于整个集合。

  对于经过POST请求建立资源的状况,须要在响应头中包含一个关联新建对象连接的Location

封装响应

   服务器能够在响应中同时返回HTTP状态码和body。有许多JavaScript框架没有把HTTP状态响应码返回给最终的开发者,这每每会致使客户端没法根据状态码来肯定具体的行为。此外,虽然HTTP规范中有不少种响应码,可是每每只有少数客户端会关心这些——一般你们只在意"success"、"error"或"failture"。所以,将响应内容和响应状态码封装在包含响应信息的表征中,是有必要的。

  OmniTI 实验室有这样一个提议,它被称为JSEND响应。更多信息请参考http://labs.omniti.com/labs/jsend。另一个提案是由Douglas Crockford提出的,能够查看这里http://www.json.org/JSONRequest.html

  这些提案在实践中并无彻底涵盖全部的状况。基本上,如今最好的作法是依照如下属性封装常规(非JSONP)响应:

  • code——包含一个整数类型的HTTP响应状态码。
  • status——包含文本:"success","fail"或"error"。HTTP状态响应码在500-599之间为"fail",在400-499之间为"error",其它均为"success"(例如:响应状态码为1XX、2XX和3XX)。
  • message——当状态值为"fail"和"error"时有效,用于显示错误信息。参照国际化(il8n)标准,它能够包含信息号或者编码,能够只包含其中一个,或者同时包含并用分隔符隔开。
  • data——包含响应的body。当状态值为"fail"或"error"时,data仅包含错误缘由或异常名称。

  下面是一个返回success的封装响应:

{
  "code": 200,
  "status": "success",
  "data": {
    "lacksTOS": false,
    "invalidCredentials": false,
    "authToken": "4ee683baa2a3332c3c86026d"
  }
}

  返回error的封装响应:

{
  "code": 401,
  "status": "error",
  "message": "token is invalid",
  "data": "UnauthorizedException"
}

  这两个封装响应对应的XML以下:

<response>
    <code>200</code>
    <status>success</status>
    <data class="AuthenticationResult">
        <lacksTOS>false</lacksTOS>
        <invalidCredentials>false</invalidCredentials>
        <authToken>1.0|idm|idm|4ee683baa2a3332c3c86026d</authToken>
    </data>
</response>

  和:

<response>
    <code>401</code>
    <status>error</status>
    <message>token is invalid</message>
    <data class="string">UnauthorizedException</data>
</response>

处理跨域问题

   咱们都据说过有关浏览器的同源策略或同源性需求。它指的是浏览器只能请求当前正在显示的站点的资源。例如,若是当前正在显示的站点是www.Example1.com,则该站点不能对www.Example.com发起请求。显然这会影响站点访问服务器的方式。

  目前有两个被普遍接受的支持跨域请求的方法:JSONP和跨域资源共享(CORS)。JSONP或“填充的JSON”是一种使用模式,它提供了一个方法请求来自不一样域中的服务器的数据。其工做方式是从服务器返回任意的JavaScript代码,而不是JSON。客户端的响应由JavaScript解析器进行解析,而不是直接解析JSON数据。另外,CORS是一种web浏览器的技术规范,它为web服务器定义了一种方式,从而容许服务器的资源能够被不一样域的网页访问。CORS被看作是JSONP的最新替代品,而且能够被全部现代浏览器支持。所以,不建议使用JSONP。任何状况下,推荐选择CORS。

支持CORS

  在服务端实现CORS很简单,只须要在发送响应时附带HTTP头,例如: 

Access-Control-Allow-Origin: *

  只有在数据是公共使用的状况下才会将访问来源设置为"*"。大多数状况下,Access-Control-Allow-Origin头应该指定哪些域能够发起一个CORS请求。只有须要跨域访问的URL才设置CORS头。

Access-Control-Allow-Origin: http://example.com:8080 http://foo.example.com

  以上Access-Control-Allow-Origin头中,被设置为只容许受信任的域能够访问。

Access-Control-Allow-Credentials: true

  只在须要时才使用上面这个header,由于若是用户已经登陆的话,它会同时发送cookies/sessions。

  这些headers能够经过web服务器、代理来进行配置,或者从服务器自己发送。不推荐在服务端实现,由于很不灵活。或者,能够使用上面的第二种方式,在web服务器上配置一个用空格分隔的域的列表。更多关于CORS的内容能够参考这里:http://enable-cors.org/

支持JSONP

  JSONP经过利用GET请求避开浏览器的限制,从而实现对全部服务的调用。其工做原理是请求方在请求的URL上添加一个字符串查询参数(例如:jsonp=”jsonp_callback”),其中“jsonp”参数的值是JavaScript函数名,该函数在有响应返回时将会被调用。

  因为GET请求中没有包含请求体,JSONP在使用时有着严重的局限性,所以数据必须经过字符串查询参数来传递。一样的,为了支持PUT,POST和DELETE方法,HTTP方法必须也经过字符串查询参数来传递,相似_method=POST这种形式。像这样的HTTP方法传送方式是不推荐使用的,这会让服务处于安全风险之中。

  JSONP一般在一些不支持CORS的老旧浏览器中使用,若是要改为支持CORS的,会影响整个服务器的架构。或者咱们也能够经过代理来实现JSONP。总之,JSONP正在被CORS所替代,咱们应该尽量地使用CORS。

  为了在服务端支持JSONP,在JSONP字符串查询参数传递时,响应必需要执行如下这些操做:

  1. 响应体必须封装成一个参数传递给jsonp中指定的JavaScript函数(例如:jsonp_callback("<JSON response body>"))。
  2. 始终返回HTTP状态码200(OK),而且将真实的状态做为JSON响应中的一部分返回。

  另外,响应体中经常必须包含响应头。这使得JSONP回调方法须要根据响应体来肯定响应处理方式,由于它自己没法得知真实的响应头和状态值。

  下面的例子是按照上述方法封装的一个返回error状态的jsonp(注意:HTTP的响应状态是200):

jsonp_callback("{'code':'404', 'status':'error','headers':[],'message':'resource XYZ not
found','data':'NotFoundException'}")

  成功建立后的响应相似于这样(HTTP的响应状态还是200):

jsonp_callback("{'code':'201', 'status':'error','headers':
[{'Location':'http://www.example.com/customers/12345'}],'data':'12345'}")

 

查询,过滤和分页

  对于大数据集,从带宽的角度来看,限制返回的数据量是很是重要的。而从UI处理的角度来看,限制数据量也一样重要,由于UI一般只能展示大数据集中的一小部分数据。在数据集的增加速度不肯定的状况下,限制默认返回的数据量是颇有必要的。以Twitter为例,要获取某个用户的推文(经过我的主页的时间轴),若是没有特别指定,请求默认只会返回20条记录,尽管系统最多能够返回200条记录。

  除了限制返回的数据量,咱们还须要考虑如何对大数据集进行“分页”或下拉滚动操做。建立数据的“页码”,返回大数据列表的已知片断,而后标出数据的“前一页”和“后一页”——这一行为被称为分页。此外,咱们可能也须要指定响应中将包含哪些字段或属性,从而限制返回值的数量,而且咱们但愿最终可以经过特定值来进行查询操做,并对返回值进行排序。

  有两种主要的方法来同时限制查询结果和执行分页操做。首先,咱们能够创建一个索引方案,它能够以页码为导向(请求中要给出每一页的记录数及页码),或者以记录为导向(请求中直接给出第一条记录和最后一条记录)来肯定返回值的起始位置。举个例子,这两种方法分别表示:“给出第五页(假设每页有20条记录)的记录”,或“给出第100到第120条的记录”。

  服务端将根据运做机制来进行切分。有些UI工具,好比Dojo JSON会选择模仿HTTP规范使用字节范围。若是服务端支持out of box(即开箱即用功能),则前端UI工具和后端服务之间无需任何转换,这样使用起来会很方便。

  下文将介绍一种方法,既可以支持Dojo这样的分页模式(在请求头中给出记录的范围),也能支持使用字符串查询参数。这样一来服务端将变得更加灵活,既能够使用相似Dojo同样先进的UI工具集,也能够使用简单直接的连接和标签,而无需再为此增长复杂的开发工做。但若是服务不直接支持UI功能,能够考虑不要在请求头中给出记录范围。

  要特别指出的是,咱们并不推荐在全部服务中使用查询、过滤和分页操做。并非全部资源都默认支持这些操做,只有某些特定的资源才支持。服务和资源的文档应当说明哪些接口支持这些复杂的功能。

结果限制

  “给出第3到第55条的记录”,这种请求数据的方式和HTTP的字节范围规范更一致,所以咱们能够用它来标识Range header。而“从第2条记录开始,给出最多20条记录”这种方式更易于阅读和理解,所以咱们一般会用字符串查询参数的方式来表示。

  综上所述,推荐既支持使用HTTP Range header,也支持使用字符串查询参数——offset(偏移量)和limit(限制),而后在服务端对响应结果进行限制。注意,若是同时支持这两种方式,那么字符串查询参数的优先级要高于Range header。

  这里你可能会有个疑问:“这两种方法功能类似,可是返回的数据不彻底一致。这会不会让人混淆呢?”恩…这是两个问题。首先要回答的是,这的确会让人混淆。关键是,字符串查询参数看起来更加清晰易懂,在构建和解析时更加方便。而Range header则更可能是由机器来使用(偏向于底层),它更加符合HTTP使用规范。

  总之,解析Range header的工做会增长复杂度,相应的客户端在构建请求时也须要进行一些处理。而使用单独的limit和offset参数会更加容易理解和构建,而且不须要对开发人员有更多的要求。

用范围标记进行限制

  当用HTTP header而不是字符串查询参数来获取记录的范围时,Ranger header应该经过如下内容来指定范围: 

  Range: items=0-24

  注意记录是从0开始的连续字段,HTTP规范中说明了如何使用Range header来请求字节。也就是说,若是要请求数据集中的第一条记录,范围应当从0开始算起。上述的请求将会返回前25个记录,假设数据集中至少有25条记录。

  而在服务端,经过检查请求的Range header来肯定该返回哪些记录。只要Range header存在,就会有一个简单的正则表达式(如"items=(\d+)-(\d+)")对其进行解析,来获取要检索的范围值。

用字符串查询参数进行限制

  字符串查询参数被做为Range header的替代选择,它使用offset和limit做为参数名,其中offset表明要查询的第一条记录编号(与上述的用于范围标记的items第一个数字相同),limit表明记录的最大条数。下面的例子返回的结果与上述用范围标记的例子一致:

  GET http://api.example.com/resources?offset=0&limit=25

  Offset参数的值与Range header中的相似,也是从0开始计算。Limit参数的值是返回记录的最大数量。当字符串查询参数中未指定limit时,服务端应当给出一个缺省的最大limit值,不过这些参数的使用都须要在文档中进行说明。

基于范围的响应

  对一个基于范围的请求来讲,不管是经过HTTP的Range header仍是经过字符串查询参数,服务端都应该有一个Content-Range header来响应,以代表返回记录的条数和总记录数:

  Content-Range: items 0-24/66

  注意这里的总记录数(如本例中的66)不是从0开始计算的。若是要请求数据集中的最后几条记录,Content-Range header的内容应该是这样:

  Content-Range: items 40-65/66

  根据HTTP的规范,若是响应时总记录数未知或难以计算,也能够用星号("*")来代替(如本例中的66)。本例中响应头也可这样写:

  Content-Range: items 40-65/*

  不过要注意,Dojo或一些其它的UI工具可能不支持该符号。

分页

  上述方式经过请求方指定数据集的范围来限制返回结果,从而实现分页功能。上面的例子中一共有66条记录,若是每页25条记录,要显示第二页数据,Range header的内容以下:

  Range: items=25-49

  一样,用字符串查询参数表示以下:

  GET …?offset=25&limit=25

  服务端会相应地返回一组数据,附带的Content-Range header内容以下:

  Content-Range: 25-49/66

  在大部分状况下,这种分页方式都没有问题。但偶尔会有这种状况,就是要返回的记录数量没法直接表示成数据集中的行号。还有就是有些数据集的变化很快,不断会有新的数据插入到数据集中,这样必然会致使分页出现问题,一些重复的数据可能会出如今不一样的页中。

  按日期排列的数据集(例如Twitter feed)就是一种常见的状况。虽然你仍是能够对数据进行分页,但有时用"after"或"before"这样的关键字并与Range header(或者与字符串查询参数offset和limit)配合来实现分页,看起来会更加简洁易懂。

  例如,要获取给定时间戳的前20条评论:

  GET http://www.example.com/remarks/home_timeline?after=<timestamp> 

  Range: items=0-19

  GET http://www.example.com/remarks/home_timeline?before=<timestamp> 

  Range: items=0-19

  用字符串查询参数表示为:

  GET http://www.example.com/remarks/home_timeline?after=<timestamp>&offset=0&limit=20 

  GET http://www.example.com/remarks/home_timeline?before=<timestamp>&offset=0&limit=20

  有关在不一样状况对时间戳的格式化处理,请参见下文的“日期/时间处理”。

  若是请求时没有指定要返回的数据范围,服务端返回了一组默认数据或限定的最大数据集,那么服务端同时也应该在返回结果中包含Content-Range header来和客户端进行确认。以上面我的主页的时间轴为例,不管客户端是否指定了Range header,服务端每次都只返回20条记录。此时,服务端响应的Content-Range header应该包含以下内容:

  Content-Range: 0-19/4125

  或 Content-Range: 0-19/*

结果的过滤和排序

  针对返回结果,还须要考虑如何在服务端对数据进行过滤和排列,以及如何按指定的顺序对子数据进行检索。这些操做能够与分页、结果限制,以及字符串查询参数filter和sort等相结合,能够实现强大的数据检索功能。

  再强调一次,过滤和排序都是复杂的操做,不须要默认提供给全部的资源。下文将介绍哪些资源须要提供过滤和排序。

过滤

  在本文中,过滤被定义为“经过特定的条件来肯定必需要返回的数据,从而减小返回的数量”。若是服务端支持一套完整的比较运算符和复杂的条件匹配,过滤操做将变得至关复杂。不过咱们一般会使用一些简单的表达式,如starts-with(以...开始)或contains(包含)来进行匹配,以保证返回数据的完整性。

  在咱们开始讨论过滤的字符串查询参数以前,必须先明白为何要使用单个参数而不是多个字符串查询参数。从根本上来讲是为了减小参数名称的冲突。咱们已经有offsetlimitsort(见下文)参数了。若是可能的话还会有jsonpformat标识符,或许还会有afterbefore参数,这些都是在本文中提到过的字符串查询参数。字符串查询中使用的参数越多,就越可能致使参数名称的冲突,而使用单个过滤参数则会将冲突的可能性降到最低。

  此外,从服务端也很容易仅经过单个的filter参数来判断请求方是否须要数据过滤功能。若是查询需求的复杂度增长,单个参数将更具备灵活性——能够本身创建一套功能完整的查询语法(详见下文OData注释或访问http://www.odata.org)。

  经过引入一组常见的、公认的分隔符,用于过滤的表达式能够以很是直观的形式被使用。用这些分隔符来设置过滤查询参数的值,这些分隔符所建立的参数名/值对可以更加容易地被服务端解析并提升数据查询的性能。目前已有的分隔符包括用来分隔每一个过滤短语的竖线("|")和用来分隔参数名和值的双冒号("::")。这套分隔符足够惟一,并适合大多数状况,同时用它来构建的字符串查询参数也更加容易理解。下面将用一个简单的例子来介绍它的用法。假设咱们想要给名为“Todd”的用户们发送请求,他们住在丹佛,有着“Grand Poobah”之称。用字符串查询参数实现的请求URI以下:

  GET http://www.example.com/users?filter="name::todd|city::denver|title::grand poobah"

  双冒号("::")分隔符将属性名和值分开,这样属性值就可以包含空格——服务端能更容易地从属性值中解析出分隔符。

  注意查询参数名/值对中的属性名要和服务端返回的属性名相匹配。

  简单而有效。有关大小写敏感的问题,要根据具体状况来看,但总的来讲,在不用关心大小写的状况下,过滤功能能够很好地运做。若查询参数名/值对中的属性值未知,你也能够用星号("*")来代替。

  除了简单的表达式和通配符以外,若要进行更复杂的查询,你必需要引入运算符。在这种状况下,运算符自己也是属性值的一部分,可以被服务端解析,而不是变为属性名的一部分。当须要复杂的query-language-style(查询语言风格)功能时,可参考Open Data Protocol (OData) Filter System Query Option说明中的查询概念(详见http://www.odata.org/documentation/uriconventions#FilterSystemQueryOption)。

排序

  排序决定了从服务端返回的记录的顺序。也就是对响应中的多条记录进行排序。

  一样,咱们这里只考虑一些比较简单的状况。推荐使用排序字符串查询参数,它包含了一组用分隔符分隔的属性名。具体作法是,默认对每一个属性名按升序排列,若是属性名有前缀"-",则按降序排列。用竖线("|")分隔每一个属性名,这和前面过滤功能中的参数名/值对的作法同样。

  举个例子,若是咱们想按用户的姓和名进行升序排序,而对雇佣时间进行降序排序,请求将是这样的:

  GET http://www.example.com/users?sort=last_name|first_name|-hire_date

  再次强调一下,查询参数名/值对中的属性名要和服务端返回的属性名相匹配。此外,因为排序操做比较复杂,咱们只对须要的资源提供排序功能。若是须要的话也能够在客户端对小的资源集合进行排列。

 

服务版本管理

   坦率地讲,一说到版本就会让人以为很困难,很麻烦,不太容易,甚至会让人以为难受——由于这会增长API的复杂度,并同时可能会对客户端产生一些影响。所以,在API的设计中要尽可能避免多个不一样的版本。

  不支持版本,不将版本控制做为糟糕的API设计的依靠。若是你在APIs的设计中引入版本,这早晚都会让你抓狂。因为返回的数据经过JSON来呈现,客户端会因为不一样的版本而接收到不一样的属性。这样就会存在一些问题,如从内容自己和验证规则方面改变了一个已存在的属性的含义。

  固然,咱们没法避免API可能在某些时候须要改变返回数据的格式和内容,而这也将致使消费端的一些变化,咱们应当避免进行一些重大的调整。将API进行版本化管理是避免这种重大变化的一种有效方式。

经过内容协商支持版本管理

  以往,版本管理经过URI自己的版本号来完成,客户端在请求的URI中标明要获取的资源的版本号。事实上,许多大公司如Twitter、Yammer、Facebook、Google等常常在他们的URI里使用版本号。甚至像WSO2这样的API管理工具也会在它的URLs中要求版本号。

  面向REST原则,版本管理技术飞速发展。由于它不包含HTTP规范中内置的header,也不支持仅当一个新的资源或概念被引入时才应该添加新URI的观点——即版本不是表现形式的变化。另外一个反对的理由是资源URI是不会随时间改变的,资源就是资源。

  URI应该能简单地识别资源——而不是它的“形状”(状态)。另外一个就是必须指定响应的格式(表征)。还有一对HTTP headers:Accept 和 Content-Type。Accept header容许客户端指定所但愿或能支持的响应的媒体类型(一种或多种)。Content-Type header可分别被客户端和服务端用来指定请求或响应的数据格式。

  例如,要获取一个user的JSON格式的数据:

  #Request:

  GET http://api.example.com/users/12345
  Accept: application/json; version=1

  #Response:

  HTTP/1.1 200 OK
  Content-Type: application/json; version=1

  {"id":"12345", "name":"Joe DiMaggio"}

  如今,咱们对同一资源请求版本2的数据:

  #Request:

  GET http://api.example.com/users/12345
  Accept: application/json; version=2

  #Response:

  HTTP/1.1 200 OK
  Content-Type: application/json; version=2

  {"id":"12345", "firstName":"Joe", "lastName":"DiMaggio"}

  Accept header被用来表示所指望的响应格式(以及示例中的版本号),注意以上两个相同的URI是如何作到在不一样的版本中识别资源的。或者,若是客户端须要一个XML格式的数据,能够将Accept header设置为"application/xml",若是须要的话也能够带一个指定的版本号。

  因为Accept header能够被设置为容许多种媒体类型,在响应请求时,服务器将把响应的Content-Type header设置为最匹配客户端请求内容的类型。更多信息能够参考http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.Html

  例如:

  #Request

  GET http://api.example.com/users/12345

  Accept: application/json; version=1, application/xml; version=1

  上述请求中,假设服务器支持JSON 和XML格式的请求,或者两种都支持,那么将由服务器来决定最终返回哪一种类型的数据。但不管服务器选择哪种,都会在响应中包含Content-Type header。

  例如,若是服务器返回application/xml格式的数据,结果是:

  #Response

  HTTP/1.1 200 OK
  Content-Type: application/xml; version=1

  <user>
    <id>12345</id>
    <name>Joe DiMaggio</name>
  </user>

  为了说明Content-Type在发送数据给服务器时的用处,这里给出一个用JSON格式建立新用户的例子:

  #Request

  POST http://api.example.com/users
  Content-Type: application/json;version=1

  {"name":"Marco Polo"}

  或者,调用版本2的接口:

  #Request

  POST http://api.example.com/users
  Content-Type: application/json;version=2

  {"firstName":"Marco", "lastName":"Polo"}

当没有指定版本时,返回什么版本?

  并不须要在每个请求中都指定版本号。因为HTTP content-negotiation(内容协商)遵循类型的“最佳匹配”方式,因此你的API也应该遵循这一点。根据这一原则,当客户端没有指定版本时,API应当返回所支持的最先版本。

  仍是这个例子,获取一个user的JSON格式的数据:

  #Request

  GET http://api.example.com/users/12345
  Accept: application/json

  #Response

  HTTP/1.1 200 OK
  Content-Type: application/json; version=1

  {"id":"12345", "name":"Joe DiMaggio"}

  相应地,当以POST方式向服务器发送数据时,若是服务器支持多个不一样版本,而请求时又没有指定版本,和上面的例子同样——服务器会将最小/最先版本的数据包含在body中。为了进行说明,下面的例子以JSON格式请求一个包含多版本资源的服务器,来建立一个新用户(预期会返回版本1):

  #Request

  POST http://api.example.com/users
  Content-Type: application/json

  {"name":"Marco Polo"}

  #Response

  HTTP/1.1 201 OK
  Content-Type: application/json; version=1
  Location: http://api.example.com/users/12345

  {"id":"12345", "name":"Marco Polo"}

请求不支持的版本

  当请求一个不支持的版本号时(包含在API生命周期中已经消失的资源版本),API应当返回一个错误的HTTP状态码406(表示不被接受)。此外,API还应当返回一个带有Content-Type: application/json的响应体,其中包含一个JSON数组,用于说明该服务器支持的类型。

  #Request

  GET http://api.example.com/users/12345
  Content-Type: application/json; version=999

  #Response

  HTTP/1.1 406 NOT ACCEPTABLE 

  Content-Type: application/json

  ["application/json; version=1", "application/json; version=2", "application/xml; version=1", "application/xml; version=2"]

何时应该建立一个新版本?

  API开发中的不少方面都会打破约定,并最终对客户端产生一些不良影响。若是你不肯定API的修改会带来怎样的后果,保险起见最好考虑使用版本控制。当你在考虑提供一个新版本是否合适时,或者考虑对现有的返回表征进行修改是否必定能知足须要并被客户端所接受时,有这样几个因素要考虑。

破坏性的修改

  • 改变属性名(例如将"name"改为"firstName")
  • 删除属性
  • 改变属性的数据类型(例如将numeric变为string, boolean变为bit/numeric,string 变为 datetime等等)
  • 改变验证规则
  • 在Atom样式的连接中,修改"rel"的值
  • 在现有的工做流中引入必要资源
  • 改变资源的概念/意图;概念/意图或资源状态的意义不一样于它原始的意义。例如:
    • 一个content type是text/html的资源,以前表示的是全部支持的媒体类型的一个"links"集合,而新的text/html则表示的是用户输入的“web浏览器表单”。
    • 一个带有"endTime"参数的API,对资源"…/users/{id}/exams/{id}"表达的含义是学生在那个时间提交试卷,而新的含义则是考试的预约结束时间。
  • 经过添加新的字段来改变现有的资源。将两个资源合并为一个并弃用原有的资源。
    • 有这样两个资源"…/users/{id}/dropboxBaskets/{id}/messages/{id}"和"…/users/{id}/dropboxBaskets/{id}/messages/{id}/readStatus"。新需求是把readStatus资源的属性放到单独的message资源中,并弃用readStatus资源。这将致使messages资源中指向readStatus资源的连接被移除。

  虽然上面列出的并不全面,但它给出了一些会对客户端产生破坏性影响的变化类型,这时须要考虑提供一个新资源或新版本。

非破坏性的修改

  • 在返回的JSON中添加新属性
  • 添加指向其它资源的"link"
  • 添加content-type支持的新格式
  • 添加content-language支持的新格式
  • 因为API的建立者和消费者都要处理不一样的casing,所以casing的变化可有可无

版本控制应在什么级别出现?

  建议对单个的资源进行版本控制。对API的一些改变,如修改工做流,也许要跨多个资源的版本控制,以此来防止对客户端产生破坏性的影响。

利用Content-Location来加强响应

  可选。见RDF(Resource Description Framework,即资源描述框架)规范。

带有Content-Type的连接

  Atom风格的连接支持"type"属性。提供足够的信息以便客户端能够对特定的版本和内容类型进行调用。

找出支持的版本

我应该同时支持多少个版本?

  维护多个不一样的版本会让工做变得繁琐、复杂、容易出错,并且代价高,对于任何给定的资源,你应该支持不超过2个版本。

弃用

  Deprecated(弃用)的目的是用来讲明资源对API仍然可用,但在未来会不存在并变得不可用。注意:弃用的时长将由弃用策略决定——这里并无给出定义。

我如何告知客户端被弃用的资源?

  许多客户端未来访问的资源可能在新版本引入后会被废弃掉,所以,他们须要有一种方法来发现和监控他们的应用程序对弃用资源的使用。当请求一个弃用资源时,API应该正常响应,并带有一个布尔类型的自定义Header "Deprecated"。如下用一个例子来进行说明。

  #Request

  GET http://api.example.com/users/12345
  Accept: application/json
  Content-Type: application/json; version=1

  #Response

  HTTP/1.1 200 OK
  Content-Type: application/json; version=1
  Deprecated: true
  {“id”:”12345”, “name”:”Joe DiMaggio”}

 

日期/时间处理

  若是没有妥善地、一致地处理好日期和时间的话,这将成为一个大麻烦。咱们常常会碰到时区的问题,并且因为日期在JSON中是以字符串的格式存在的,若是未指定统一的格式,那么解析日期也会是一个问题。

  在接口内部,服务端应该以UTC或GMT时间来存储、处理和缓存时间戳。这将有效缓解日期和时间的问题。

Body内容中的日期/时间序列化

  有一个简单的方法能够解决这些问题——在字符串中始终用相同的格式,包括时间片(带有时区信息)。ISO8601时间格式是一个不错的解决方案,它使用了彻底加强的时间格式,包括小时、分钟、秒以及秒的小数部分(例如yyyy-MM-dd'T'HH:mm:ss.SSS'Z')。建议在REST服务的body内容中(请求和响应均包括)使用ISO8601表明全部的日期格式。

  顺便提一下,对于那些基于JAVA的服务来讲,DateAdapterJ库使用DateAdapter,Iso8601TimepointAdapter和HttpHeaderTimestampAdapter类能够很是容易地解析和格式化ISO8601日期和时间,以及HTTP 1.1 header(RFC1123)格式。能够从https://github.com/tfredrich/DateAdapterJ下载。

  对于那些建立基于浏览器的用户界面来讲,ECMAScript5规范一开始就包含了JavaScript解析和建立ISO8601日期的内容,因此它应该成为咱们所说的主流浏览器所听从的方式。固然,若是你要支持那些不能自动解析日期的旧版浏览器,能够使用JavaStript库或正则表达式。这里有几个能够解析和建立ISO8601时间的JavaStript库:

  http://momentjs.com/

  http://www.datejs.com/

HTTP Headers中的日期/时间序列化

  然而上述建议仅适用于HTTP请求或响应内容中的JSON和XML内容,HTTP规范针对HTTP headers使用另外一种不一样的格式。在被RFC1123更替的RFC822中指出,该格式包括了各类日期、时间和date-time格式。不过,建议始终使用时间戳格式,在你的request headers中它看起来像这样:

  Sun, 06 Nov 1994 08:49:37 GMT

  不过,这种格式没有考虑毫秒或者秒的十进制小数。Java的SimpleDataFormat的格式串是:"EEE, dd MMM yyyy HH:mm:ss 'GMT'"。

 

保护服务的安全

  Authentication(身份认证)指的是确认给定的请求是从服务已知的某人(或某个系统)发出的,且请求者是他本身所声明的那我的。Authentication是为了验证请求者的真实身份,而authorization(受权)是为了验证请求者有权限去执行被请求的操做。

  本质上,这个过程是这样的:

  1. 客户端发起一个请求,将authentication的token(身份认证令牌)包含在X-Authentication header中,或者将token附加在请求的查询串参数中。
  2. 服务器对authorization token(受权令牌)进行检查,并进行验证(有效且未过时),并根据令牌内容解析或者加载认证主体。
  3. 服务器调用受权服务,提供认证主体、被请求资源和必要的操做许可。
  4. 若是受权经过了,服务器将会继续正常运行。

  上面第三步的开销可能会比较大,可是假设若是存在一个可缓存的权限控制列表(ACL),那么在发出远程请求前,能够在本地建立一个受权客户端来缓存最新的ACLs。

身份验证

  目前最好的作法是使用OAuth身份验证。强烈推荐OAuth2,不过它仍然处于草案状态。或者选择OAuth1,它彻底能够胜任。在某些状况下也能够选择3-Legged OAuth。更多有关OAuth的规范能够查看这里http://oauth.net/documentation/spec/

  OpenID是一个附加选择。不过建议将OpenID做为一个附加的身份验证选项,以OAuth为主。更多有关OpenID的规范能够查看这里http://openid.net/developers/specs/

传输安全

  全部的认证都应该使用SSL。OAuth2须要受权服务器和access token(访问令牌)来使用TLS(安全传输层协议)。

  在HTTP和HTTPS之间切换会带来安全隐患,最好的作法是全部通信默认都使用TLS。

受权

  对服务的受权和对任何应用程序的受权同样,没有任何区别。它基于这样一个问题:“主体是否对给定的资源有请求的许可?”这里给出了简单的三项数据(主体,资源和许可),所以很容易构造一个支持这种概念的受权服务。其中主体是被授予资源访问许可的人或系统。使用这些通常概念,就能够为每个主题构建一个缓存访问控制列表(ALC)。

应用程序安全

  对RESTful服务来讲,开发一个安全的web应用适用一样的原则。

  • 在服务器上验证全部输入。接受“已知”的正确的输入并拒绝错误的输入。
  • 防止SQL和NoSQL注入。
  • 使用library如微软的Anti-XSS或OWASP的AntiSammy来对输出的数据进行编码。
  • 将消息的长度限制在肯定的字段长度内。
  • 服务应该只显示通常的错误信息。
  • 考虑业务逻辑攻击。例如,攻击者能够跳过多步骤的订购流程来订购产品而无需输入信用卡信息吗?
  • 对可疑的活动记录日志。

  RESTful安全须要注意的地方:

  • 验证数据的JSON和XML格式。
  • HTTP动词应该被限制在容许的方法中。例如,GET请求不能删除一个实体。GET用来读取实体而DELETE用来删除实体。
  • 注意race conditions(竞争条件——因为两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能由于时间上推动的前后缘由而出现问题)。

  API网关可用于监视、限制和控制对API的访问。如下内容可由网关或RESTful服务实现。

  • 监视API的使用状况,并了解哪些活动是正常的,哪些是非正常的。
  • 限制API的使用,使恶意用户不能停掉一个API服务(DOS攻击),而且有能力阻止恶意的IP地址。
  • 将API密钥存储在加密的安全密钥库中。

 

缓存和可伸缩性

  经过在系统层级消除经过远程调用来获取请求的数据,缓存提升了系统的可扩展性。服务经过在响应中设置headers来提升缓存的能力。遗憾的是,HTTP 1.0中与缓存相关的headers与HTTP 1.1不一样,所以服务器要同时支持两种版本。下表给出了GET请求要支持缓存所必须的最少headers集合,并给出了适当的描述。

HTTP Header 描述 示例
Date 响应返回的日期和时间(RFC1123格式)。 Date: Sun, 06 Nov 1994 08:49:37 GMT
Cache-Control 响应可被缓存的最大秒数(最大age值)。若是响应不支持缓存,值为no-cache。

Cache-Control: 360

Cache-Control: no-cache

Expires 若是给出了最大age值,该时间戳(RFC1123格式)表示的是响应过时的时间,也就是Date(例如当前日期)加上最大age值。若是响应不支持缓存,该headers不存在。 Expires: Sun, 06 Nov 1994 08:49:37 GMT
Pragma 当Cache-Control为no-cache时,该header的值也被设置为no-cahche。不然,不存在。 Pragma: no-cache
Last-Modified 资源自己最后被修改的时间戳(RFC1123格式)。 Last-Modified: Sun, 06 Nov1994 08:49:37 GMT

  为了简化,这里举一个响应中的headers集合的例子。这是一个简单的对资源进行GET请求的响应,缓存时长为一天(24小时):

  Cache-Control: 86400
  Date: Wed, 29 Feb 2012 23:01:10 GMT
  Last-Modified: Mon, 28 Feb 2011 13:10:14 GMT
  Expires: Thu, 01 Mar 2012 23:01:10 GMT

  下面是一个相似的例子,不过缓存被彻底禁用:

  Cache-Control: no-cache
  Pragma: no-cache

ETag Header

  ETag header对于验证缓存数据的新旧程度颇有用,同时也有助于条件的读取和更新操做(分别为GET和PUT)。它的值是一个任意字符串,用来表明返回数据的版本。不过,对于返回数据的不一样格式,它也能够不一样——JSON格式响应的ETag与相同资源XML格式响应的ETag会不一样。ETag header的值能够像带有格式的底层域对象的哈希表(例如Java中的Obeject.hashcode())同样简单。建议为每一个GET(读)操做返回一个ETag header。另外,确保用双引号包含ETag的值,例如:

  ETag: "686897696a7c876b7e"

 

HTTP状态码(前10)

  如下是由RESTful服务或API返回的最经常使用的HTTP状态码,以及一些有关它们广泛用法的简短说明。其它HTTP状态码不太常用,它们要么更特殊,要么更高级。大多数服务套件只支持这些经常使用的状态码,甚至只支持其中的一部分,而且它们都能正常工做。

  200 (OK) —— 一般的成功状态。表示成功的最多见代码。

  201 (CREATED) ——(经过POST或PUT)建立成功。经过设置Location header来包含一个指向最新建立的资源的连接。

  204 (NO CONTENT) —— 封装过的响应没有使用,或body中没有任何内容时(如DELETE),使用该状态。

  304 (NOT MODIFIED) —— 用于有条件的GET调用的响应,以减小带宽的使用。 若是使用该状态,那么必须为GET调用设置Date、Content-Location和ETag headers。不包含响应体。

  400 (BAD REQUEST) —— 用于执行请求时可能引发无效状态的通常错误代码。如域名无效错误、数据丢失等。

  401 (UNAUTHORIZED) —— 用于缺乏认证token或认证token无效的错误代码。

  403 (FORBIDDEN) —— 未受权的用户执行操做,没有权限访问资源,或者因为某些缘由资源不可用(如时间限制等),使用该错误码。

  404 (NOT FOUND) —— 不管资源存不存在,不管是否有40一、403的限制,当请求的资源找不到时,出于安全因素考虑,服务器均可以使用该错误码来掩饰。

  409 (CONFLICT) —— 每当执行请求可能会引发资源冲突时使用。例如,存在重复的实体,当不支持级联删除时删除根对象。

  500 (INTERNAL SERVER ERROR) —— 当服务器抛出异常时,捕捉到的通常错误。

 

附加资源

书籍

  REST API Design Rulebook,Mark Masse, 2011, O’Reilly Media, Inc.

  RESTful Web Services, Leonard Richardson and Sam Ruby, 2008, O’Reilly Media, Inc.

  RESTful Web Services Cookbook, Subbu Allamaraju, 2010, O’Reilly Media, Inc.

  REST in Practice: Hypermedia and Systems Architecture, Jim Webber, et al., 2010, O’Reilly Media, Inc.

  APIs: A Strategy Guide, Daniel Jacobson; Greg Brail; Dan Woods, 2011, O’Reilly Media, Inc.

网站

  http://www.restapitutorial.com http://www.toddfredrich.com
  http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
  http://www.json.org/ https://github.com/tfredrich/DateAdapterJ
  http://openid.net/developers/specs/
  http://oauth.net/documentation/spec/
  http://www.json.org/JSONRequest.html http://labs.omniti.com/labs/jsend
  http://enable-cors.org/
  http://www.odata.org/documentation/uri-conventions#FilterSystemQueryOption
  http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
  https://developer.linkedin.com/apis
  http://developers.facebook.com/docs/reference/api/
  https://dev.twitter.com/docs/api http://momentjs.com/
  http://www.datejs.com/

 

在原翻译的基础上通过修改:http://blog.csdn.net/huayuqa/article/details/62237010

英文原文下载:RESTful Best Practices-v1 2.pdf

相关文章
相关标签/搜索