REST over HTTP
REST 是 Roy Fielding 在他的论文 (第 5 章是 REST 的表示)中提出的与协议无关的体系结构,它将 Web 浏览器的经过验证的概念概括为客户端,以便将分布式系统中的客户端与服务器分离。
为了使服务或 API 成为 RESTful,它必须遵守给定的约束,例如:
- 客户端服务器
- 无状态
- 可缓存
- 分层系统
- 统一界面
- 资源识别
- 资源代表
- 自我描述性的信息
- 超媒体
除了 Fielding 的论文中提到的限制之外,在他的博客文章中, REST API 必须是超文本驱动的 ,Fielding 澄清说只是通过 HTTP 调用服务并不能使它成为 RESTful 。因此,服务还应遵守进一步的规则,概述如下:
-
API 应遵守并且不违反基础协议。尽管大多数时候都是通过 HTTP 使用 REST,但它并不局限于此协议。
-
通过媒体类型强烈关注资源及其呈现。
-
客户不应该对 API 中的可用资源或其返回状态( 类型化资源 ) 有初步了解或假设,而是通过已发出的请求和分析的响应动态学习它们。这使服务器有机会在不破坏客户端实现的情况下轻松移动或重命名资源。
理查森成熟度模型
在理查德森成熟度模型是为了获得 RESTful Web 服务通过 HTTP REST 应用约束的方式。
Leonard Richardson 将应用程序分为以下 4 个层次:
- 0 级:使用 HTTP 进行传输
- 第 1 级:使用 URL 来识别资源
- 第 2 级:使用 HTTP 动词和状态进行交互
- 3 级:使用 HATEOAS
由于重点是资源状态的表示,因此鼓励支持同一资源的多个表示。因此,表示可以呈现资源状态的概述,而另一个表示返回相同资源的完整细节。
另请注意,给定 Fielding 约束,只有在实现 RMM 的第 3 级时,API 才有效地进行 RESTful 。
HTTP 请求和响应
HTTP 请求是:
HTTP 响应是:
- 状态,大部分时间是 2xx(成功) , 4xx(客户端错误) 或 5xx(服务器错误)之一
- 标题(键值对)
- 身体(又称有效载荷,数据)
HTTP 动词特征:
- 有身体的动词:
POST
,PUT
,PATCH
- 必须安全的动词(即不得修改资源):
GET
- 动词必须是幂等的(即多次运行时不得再次影响资源):
GET
(nullipotent),PUT
,DELETE
body safe idempotent
GET ✗ ✔ ✔
POST ✔ ✗ ✗
PUT ✔ ✗ ✔
DELETE ✗ ✗ ✔
PATCH ✔ ✗ ✗
因此,可以将 HTTP 谓词与 CRUD 函数进行比较 :
请注意, PUT
请求要求客户端使用更新的值发送整个资源。要部分更新资源,可以使用 PATCH
动词(请参阅如何部分更新资源? )。
通常的 HTTP 响应状态
成功
重定向
- 304(未修改) :客户端可以使用它具有所请求资源的缓存版本
客户错误
401(UNAUTHORIZED)
:匿名请求访问受保护的 API403(FORBIDDEN)
:经过身份验证的请求没有足够的权限来访问受保护的 API- 404( 未找到 ) :找不到资源
409(CONFLICT)
:冲突中的资源状态(例如,用户尝试使用已注册的电子邮件创建帐户)410(GONE)
:与 404 相同,但资源已存在- 412(PRECONDITION FAILED) :请求尝试修改处于意外状态的资源
- 422(不可控制的实体) :请求有效负载在语法上是有效的,但在语义上是错误的(例如,未被估值的必需字段)
- 423(已锁定) :资源已锁定
- 424(FAILED DEPENDENCY) :请求的行动取决于另一个失败的行动
- 429( 太多请求) :用户在给定的时间内发送了太多请求
- 否则: 400(BAD REQUEST)
服务器错误
- 501(NOT IMPLEMENTED) :服务器不支持完成请求所需的功能
- 503(SERVICE UNAVAILABLE) :由于临时过载或定期维护,服务器当前无法处理请求
- 507(INSUFFICIENT STORAGE) :服务器无法存储成功完成请求所需的表示
- 否则: 500(内部服务器错误)
笔记
没有什么可以阻止你为错误的反应添加正文,以使客户拒绝更清楚。例如, 422(UNPROCESSABLE ENTITY) 有点模糊:响应主体应该提供无法处理实体的原因。
HATEOAS
每个资源必须为其链接的资源提供超媒体。链接至少由以下内容组成:
rel
(for rel ation,aka name):描述主要资源和链接资源之间的关系- 一个
href
:定位链接资源的 URL
还可以使用其他属性来帮助弃用,内容协商等。
Cormac Mulhall 解释说*,客户端应根据尝试的内容决定使用什么 HTTP 动词*。如有疑问,API 文档无论如何都应该帮助你了解与所有超媒体的可用交互。
媒体类型
媒体类型有助于提供自我描述性消息。它们扮演客户端和服务器之间的合同的一部分,以便他们可以交换资源和超媒体。
尽管 application/json
和 application/xml
是非常流行的媒体类型,但它们并没有太多的语义。它们只描述了文档中使用的整体语法。应使用(或通过供应商媒体类型扩展 ) 支持 HATEOAS 要求的更专业的媒体类型 ,例如:
客户端通过将 Accept
标头添加到其请求中来告知服务器它理解哪些媒体类型,例如:
Accept: application/hal+json
如果服务器无法以这种表示形式生成所请求的资源,则返回 406(不可接受) 。否则,它会在保存所表示资源的响应的 Content-Type
标头中添加媒体类型,例如:
Content-Type: application/hal+json
无国籍的>有状态的
为什么?
有状态服务器意味着客户端会话存储在服务器实例本地存储中(几乎总是存储在 Web 服务器会话中)。尝试水平扩展时,这开始出现问题 :如果在负载均衡器后面隐藏多个服务器实例,则在登录时首先将一个客户端分派到实例#1 ,然后在获取受保护资源时将其分配给实例#2 然后第二个实例将以匿名方式处理请求,因为客户端会话已在本地存储在实例#1 中。
已经找到解决方案来解决这个问题(例如,通过配置会话复制和/或粘性会话 ),但 REST 架构提出了另一种方法:只是不要使服务器有状态,使其成为无状态。根据菲尔丁的说法 :
从客户端到服务器的每个请求都必须包含理解请求所需的所有信息,并且不能利用服务器上任何存储的上下文。因此,会话状态完全保留在客户端上。
换句话说,无论是否将调度分派给实例#1 或实例#2 ,都必须以完全相同的方式处理请求。这就是无状态应用程序被认为更容易扩展的原因。
怎么样?
常见的方法是基于令牌的身份验证 ,尤其是时尚的 JSON Web 令牌 。请注意,JWT 仍然存在一些问题,特别是关于失效和自动延长到期时间 (即记住我的功能)。
旁注
使用 cookie 或标头(或其他任何东西)与服务器是有状态还是无状态无关:这些只是用于传输令牌的媒体(有状态服务器的会话标识符,JWT 等),仅此而已。
当 RESTful API 仅供浏览器使用时,( HttpOnly 和安全 )cookie 可以非常方便,因为浏览器会自动将它们附加到传出请求。值得一提的是,如果你选择 cookie,请注意 CSRF (一种防止它的好方法是让客户端生成并在 cookie 和自定义 HTTP 头中发送相同的唯一秘密值 )。
具有条件请求的可缓存 API
随着 Last-Modified
标头
服务器可以为包含可缓存资源的响应提供 Last-Modified
日期标头 。然后,客户端应将此日期与资源一起存储。
现在,每次客户请求 API 读取资源时,他们都可以向他们的请求添加包含他们收到和存储的最新 Last-Modified
日期的 If-Modified-Since
标头 。然后,服务器将比较请求的标头和资源的实际上次修改日期。如果它们相等,则服务器返回 304(未修改) ,空体:请求客户端应使用它具有的当前缓存资源。
此外,当客户端请求 API 更新资源(即使用不安全的动词)时,他们可以添加 If-Unmodified-Since
标头 。这有助于处理竞争条件:如果标题和实际的最后修改日期不同,则服务器返回 412(PRECONDITION FAILED) 。然后,客户端应在重试修改资源之前读取资源的新状态。
随着 ETag
标头
一个的 ETag (实体标签)为资源的特定状态的标识符。它可以是用于强验证的资源的 MD5 哈希,或用于弱验证的域特定标识符。
基本上,该过程与 Last-Modified
标头相同:服务器为保存可缓存资源的响应提供 ETag
标头 ,然后客户端应将此标识符与资源一起存储。
然后,客户端在想要读取资源时提供 If-None-Match
标头 ,其中包含他们收到和存储的最新 ETag。如果标头与资源的实际 ETag 匹配,则服务器现在可以返回 304(NOT MODIFIED) 。
同样,客户端可以在想要修改资源时提供 If-Match
标头 ,如果提供的 ETag 与实际 ETag 不匹配,则服务器必须返回 412(PRECONDITION FAILED) 。
补充说明
ETag>约会
如果客户在其请求中同时提供日期和 ETag,则必须忽略该日期。来自 RFC 7232( 此处和此处 ):
如果请求包含
If-None-Match
/If-Match
头字段,则接收者必须忽略If-Modified-Since
/If-Unmodified-Since
;If-None-Match
/If-Match
中的条件被认为是If-Modified-Since
/If-Unmodified-Since
中条件的更准确的替代,并且两者仅为了与可能不实施If-None-Match
/If-Match
的旧中间人互操作而组合。
浅 ETags
此外,虽然很明显上次修改日期与资源服务器端一起保留,但 ETag 可以使用多种方法 。
通常的方法是实现浅 ETag:服务器处理请求,好像没有给出条件头,但仅在最后,它生成它将要返回的响应的 ETag(例如通过散列),并比较它与提供的一个。这相对容易实现,因为只需要一个 HTTP 拦截器(并且许多实现已经存在,具体取决于服务器)。话虽这么说,值得一提的是这种方法可以节省带宽而不是服务器性能 :
一个更深入地执行 ETag 的机制可能会提供更大的好处 -如从缓存服务的一些请求,并没有在所有执行计算 -但实现起来最肯定不会那么简单,也不是可插拔的浅方法这里描述。
常见的陷阱
我为什么不把动词放在 URL 中?
HTTP 不是 RPC : 使 HTTP 与 RPC 显着不同的原因是请求被定向到资源。毕竟,URL 代表统一资源定位器,URL 代表 URI :统一资源 Idenfitier。 URL 以你要处理的资源为目标,HTTP 方法指示你要对其执行的操作**。** HTTP 方法也称为动词 :URL 中的动词使得没有意义。请注意,HATEOAS 关系也不应包含动词,因为链接也是针对资源的。
如何部分更新资源?
由于 PUT
请求要求客户端使用更新的值发送整个资源,因此 PUT /users/123
不能用于简单地更新用户的电子邮件。正如 William Durand 在 Please 中所解释的那样。 不要像白痴一样修补。 ,提供了几种符合 REST 的解决方案:
- 公开资源的属性并使用
PUT
方法发送更新的值,因为PUT
规范 通过将具有与较大资源的一部分重叠的状态的单独标识的资源作为目标来指示部分内容更新是可能的 :
PUT https://example.com/api/v1.2/users/123/email
body:
new.email@example.com
- 使用
PATCH
请求,该请求包含一组描述资源必须如何修改的指令(例如,遵循 JSON 补丁 ):
PATCH https://example.com/api/v1.2/users/123
body:
[
{ "op": "replace", "path": "/email", "value": "new.email@example.com" }
]
- 使用包含资源部分表示的
PATCH
请求,如 Matt Chapman 的评论中所建议的 :
PATCH https://example.com/api/v1.2/users/123
body:
{
"email": "new.email@example.com"
}
那些不适合 CRUD 操作世界的行为呢?
引用 Vinay Sahni 设计实用 RESTful API 的最佳实践 :
这是事情变得模糊的地方。有很多方法:
将操作重组为显示为资源字段。如果操作不采用参数,则此方法有效。例如,激活动作可以映射到布尔
activated
字段,并通过 PATCH 更新到资源。将其视为具有 RESTful 原则的子资源。例如,GitHub 的 API 可以让你出演一个要点与
PUT /gists/:id/star
和星标有DELETE /gists/:id/star
。有时你真的无法将动作映射到合理的 RESTful 结构。例如,多资源搜索实际上没有意义应用于特定资源的端点。在这种情况下,即使它不是资源,
/search
也会最有意义。这没关系 - 只需从 API 使用者的角度做正确的事情,并确保明确记录以避免混淆。
常见做法
-
API 已记录在案。工具可用于帮助你构建文档,例如 Swagger 或 Spring REST Docs 。
-
API 通过标头或 URL 进行版本控制 :
https://example.com/api/v1.2/blogs/123/articles
^^^^
- 资源有多个名称 :
https://example.com/api/v1.2/blogs/123/articles
^^^^^ ^^^^^^^^
- 网址使用 kebab-case (单词是小写的,以破折号分隔):
https://example.com/api/v1.2/quotation-requests
^^^^^^^^^^^^^^^^^^
- HATEOAS 提供资源的 自我链接 ,以自己为目标:
{
...,
_links: {
...,
self: { href: "https://example.com/api/v1.2/blogs/123/articles/789" }
^^^^
}
}
- HATEOAS 关系使用 lowerCamelCase(单词是小写的,然后大写,除了第一个,省略空格),以允许 JavaScript 客户端在访问链接时尊重 JavaScript 命名约定时使用点表示法 :
{
...,
_links: {
...,
firstPage: { "href": "https://example.com/api/v1.2/blogs/123/articles?pageIndex=1&pageSize=25" }
^^^^^^^^^
}
}