文章目录
- 目录
- REST
- RESTful API
- RESTful API 设计规范
-
- URI
- Request
- Response
- 登录认证问题
- 动作类型的资源定义
- HATEOAS
REST
早古时期,软件和网络是两个不同的领域,前者围绕着单机环境展开,而后者则研究系统之间的通信。随着互联网的兴起,使得这两个领域开始融合,首当其冲的就是基于 HTTP 协议的 Web 服务,越来越多的人开始意识到,“网站” 即是 “软件”。
其中的先驱者就是 Tim Berners-Lee(万维网的发明者,万维网联盟负责人)和 Roy Thomas Fielding(1996 HTTP/1.0、1999 HTTP/1.1 的主要设计者之一,Apache 基金会的第一任主席,Apache Web Server 和 HTTP 协议是共生共荣的关系)等人。
1989年,Tim Berners-Lee 在论文中提出可以在互联网上构建超链接文档,并提出了三点基本要素:
- URI(Uniform Resource Identifier):统一资源标识符,是资源(Resource)在互联网中的唯一标识。
- HTML(Hyper Text Markup Language):超文本标记语言,超文本文档是由 HTML 标签组成的描述性文本,HTML 标签将文字,图形、动画、声音、表格、链接等内容格式进行了统一。
- HTTP(Hyper Text Transfer Protocol):超文本传输协议,传输超文本文档的传输协议,传输的数据主体称为 Message(消息)。
首先我们需要对 Resource 的概念有一个清晰的理解。所谓 Resource,就是互联网上的一个实体,或者叫具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体实物的抽象。在互联网中使用 URI 来唯一标记一个 Resource,包含了 URL 和 URN。
- URL(Uniform Resource Loader):统一资源定位符,侧重于 “定位” 二字。
- URN(Uniform Resource Name):统一资源名称,侧重于 “命名” 二字。
举个例子:寻找一个具体的人(URI)。使用地址来寻找就是 URL:XX省XX市XX区XX单元XX室的主人;使用身份证号和名字来寻找就是 URN:身份证号 + 名字(但无法确认人的地址)。两者各有场景,但需要注意的是 URL、URN 都是 URI 的子集,但日常生活中最常见的是 URL,所以大家口头上也习惯使用 URL 来说明一个 Resource。但在实际编码中,开发者仍要注意 URI 和 URL 的本质区别,注意规范选词。
REST(Representational State Transfer,表现层状态转移)最初被 Roy Thomas Fielding 在 2000 年的博士论文《Architectural Styles and the Design of Network-based Software Architectures(架构风格和基于网络的软件架构设计)》中提出。顾名思义,Roy Thomas Fielding 在这篇论文中主要讨论的是:如何在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。
可见,REST、HTTP 协议、Web 网站,三者之间天生联系紧密。基于此,我们要清晰理解 Representational State Transfer 含义的前提是要对 HTTP 协议有一定的认识。
Representational State Transfer 的含义:
- Representational(表现层):即 Resources 的表现层。上述我们知道 Resource 是一种抽象,它具有多种表现形式,而最终把 Resource 具体呈现出来的形式,就叫做它的 “表现层”。例如:一个文本资源可以使用 txt 格式表现,也可以使用 HTML、XML、JSON 等格式表现。在 HTTP/1.0 中,Roy Thomas Fielding 为 HTTP Header 设计了 Accept/Content-Type 字段来描述这个 Resource 的 Representational。
所以,需要注意的是:URI 仅用于表示一个 Resource,不应该在 URI 中描述表现层的内容,一个优雅的 RESTful API 应该使用 Accept/Content-Type 字段来描述 Resource 的表现层。
Accept: Application/json
Content-Type: Application/json
- State Transfer(状态转移):Resource 作为 C/S 交互的实体,必然存在着状态的变化,然而 HTTP 是一种无状态的协议(不传输资源状态的描述)。这意味着:Resource 的状态都保存在服务端,客户端想要操作某个 Resource(改变其状态)就必须通过某种手段让 Resource 在服务端发生状态的 “转移”,而且这种 “转移” 必然是建立在 Resource 的 “表现层” 之上的,例如:创建一个图片资源、删除一个图片资源;启动一个服务,关闭一个服务。故称之为 “表现层状态转移”。
而客户端使用的手段就是 Roy Thomas Fielding 在 HTTP 请求行中设计的 Request Methods(在 HTTP/0.9 中首次引入 GET 方法,在 HTTP/1.0 中首次引入了 POST、Head 方法,在 HTTP/1.1 中引入了 PUT 方法)。
# 客户端
GET /resource
Accept: text/html
# 服务端
Content-Type: text/html;charset=utf-8
Response code: 200
至此,我们回头再看,REST 讨论的其实是一个:如何将软件和网络两个领域进行交叉融合,继而得到一个功能强、性能好、适宜通信的互联网软件架构的问题。
虽然 REST 架构在今天(2020)的软件设计中随处可见,但了解其诞生的历史背景和发展历程,还是会对这些先驱者们产生由衷的敬意。
RESTful API*-ful 在西文语境中常用于表示一种风格,RESTful API 就是符合 REST 架构设计思想的软件 API 风格。
笔者最早认识到 RESTful API,源于时任 AWS CTO 的一封内部邮件,那时是 2006 年,正值 AWS 孵化的初期,印象非常深刻的有两点:
- 强调 AWS 必须是一种 Resilient software architecture(具有韧性/弹性的软件架构)。
- 不使用 RESTful API 的员工将被辞退。
直到 2013 年笔者接触 OpenStack(OpenStack 初期常被认为是 AWS 的开源对标版本)之后才更深刻的体会到了其内涵和精髓。一言以蔽之就是:大型分布式软件的各个组件之间必须具备解耦和扩展的能力,网络(Network-base)是组件之间通信的唯一依赖,且对资源的操作具有唯一的确定性。
退一步的,我们可以选取一个角度来比较一下 RESTful 与另外两种常见的分布式架构风格的区别:
- RESTful 抽象的是资源,资源的抽象不需要依赖开发平台或编程语言,客户端和服务端完全松耦合。
- DO(Distributed Objects,分布式对象)抽象的是对象,而不同的编程语言对对象的定义有很大差别,所以 DO 架构通常会与某种编程语言(.NET)绑定,若跨语言交互,实现则会非常复杂。架构实例有 RMI、EJB/DCOM、.NET Remoting 等。
- RPC(Remote Procedure Call,远程过程调用)抽象的是过程,这要求客户端和服务端具有很强的耦合度,否者双方无法理解对方的意图。所以 RESTful 只需要以名词为核心的,而 RPC 则需要以动词为核心。架构实例有 SOAP、XML-RPC、Flash AMF 等。
AWS 奉行的这一铁律,使其得以在几年间飞速扩展至上百个核心组件,成为了极具韧性和良好生态的公有云架构。现在的软件公司基本都会采用 RESTful API 风格,让产品可以通过 API 的方式融入到行业生态中,形成 APIs 经济效益。
RESTful API 设计规范核心原则只有一句话:总是围绕着 Resource 进行建模,HTTP URI 标识资源,HTTP Request Methods 操作资源。
URI
- 资源命名使用全小写字母。
- 资源命名尽量使用复数名词,名词间使用 “-” 分隔。
- 资源命名尽量与关系型数据库表结构命名一致。
- URI 突出版本号。
- URI 不具有表现层描述。
- URL 使用 “/” 划分层级,同时尽量避免使用多级 URI。
Request
使用 HTTP Request Methods 时,要注意方法的 “安全性” 和 “幂等性”:
- 安全性:指调用 HTTP Request Method,是否会导致资源状态变化。
- 幂等性:指多次调用 HTTP Request Method,只要输入不变,那么执行的结果也是不变的。
Method | 功能 | 描述 | 安全性 | 幂等 |
---|---|---|---|---|
GET | SELECT | 获取一个(提供资源 ID)或多个(提供 Filter 条件)或全部资源,获取多个时使用 Query Parameters 进行过滤查询,使用 Pagination Parameters(e.g. limit、offset、page、sortby)进行分页查询。 | √ | √ |
POST | CREATE | 创建一个资源(杜绝一次创建多个资源)。 | X | X |
PUT | UPDATE | 更新一个资源(提供完整的资源数据)。 | X | √ |
PATCH | UPDATE | 更新一个资源(提供部分的资源数据)。 | X | √ |
DELETE | DELETE | 删除一个资源。 | X | √ |
HEAD | 获取资源的元数据而非资源本身。此方法经常被用来测试超文本链接的有效性,可访问性,和最近的改变,例如:获取虚拟机镜像的属性信息。 | √ | √ | |
OPTIONS | 获取信息,关于资源的哪些属性是由客户端决定的。在跨域或使用代理请求时,通常会用到,OPTION 请求在于判定资源的选项或需求,或者服务器的能力。 | √ | √ |
Response
Method | 功能 | 描述 |
---|---|---|
GET | SELECT | 返回一个或多个资源的完整数据和 Status Code,返回一个时使用 {},返回多个时使用 [];若错误需要返回错误原因和正确提示。 |
POST | CREATE | 返回一个资源的完整数据和 Status Code;若错误需要返回错误原因和正确提示。 |
PUT | UPDATE | 返回一个资源的完整数据和 Status Code;若错误需要返回错误原因和正确提示。 |
PATCH | UPDATE | 返回一个资源的完整数据和 Status Code;若错误需要返回错误原因和正确提示。 |
DELETE | DELETE | 仅返回 Status Code;若错误需要返回错误原因和正确提示。 |
HTTP Response Status Codes(状态码)就是一个三位数,分成五个类别:
-
1xx:相关信息(不常用)
-
2xx:操作成功
-
3xx:重定向(不常用)
-
4xx:客户端错误
-
5xx:服务器错误
登录认证是 RESTful API 设计中的一个特殊课题,登陆验证源于用户使用 Web 应用时记录用户身份状态的需求,其特点是:
- 客户端和服务端都记录了用户的账户信息,客户端请求时携带账户信息到服务端进行身份认证。
- 认证成功后,一次登录持久在线,或者设定登陆状态的过期时间。
在 RESTful 设计中通常使用 Cookies 或 Token 的方式来实现登陆验证的需求:
Cookie 方式:因为 HTTP 协议是无状态的,所以一般使用 Cookie 来解决会话状态的保存,以弥补无法进行会话跟踪的不足。
- 在登陆时,在服务端进行身份认证,通过则创建 Session,记录用户登陆状态及相关信息,并将 Session ID 返回;
- 客户端接收响应后,将 Session ID 存放在 Cookie 中(浏览器可自动记录并存储);
- 客户端的后续请求,直接在 Request Header 中携带对应 Cookie 中存放的 Session ID;
- 服务端接收请求后,验证 Session ID 是否合法,若合法则进行相关处理并给出响应。
Token 方式:
- 在登陆时,在服务端进行身份认证,通过则返回 Token,Token 具有过期时间。
- 客户端的后续请求,都携带者 Token 进行请求。返回client后,client需要通过脚本控制存放token信息;
显然,Token 是更加推荐的方式,因为 Cookie 方式会在 HTTP Header 中保持一个状态(Session ID)。这个状态会要求该请求只能被存储了对应的 Session 进行处理,这一点违背了 REST 架构的原则。
动作类型的资源定义现实情况中,总有一些场景(资源)是 HTTP Request Methods 所抽象不了的。上述的登陆验证是一个典型的例子。针对登陆验证场景,可以把用户在远程服务器的会话信息抽象为一个资源,这样的话,登陆动作其实就是在远程服务器增加了一个会话资源,反正,登出就是删除一个会话资源,所以 RESTful API 可以这样设计:
[POST] /login
[DELETE] /logout
再比如,网上汇款场景,将汇款的动作定义为一种服务:
[POST] /smsService
{"mobile":"13813888888","text":"hello world"}
再比如,OpenStack 的虚拟机操作 start、stop、reboot、migration 等,将这些操作定义为一个 os-action 资源,然后通过不同在 Request Body 中使用不同的内容来进行区分:
[POST] /servers/{uuid}
{"os-action": "start"}
{"os-action": "stop"}
简而言之,如果某些动作是 HTTP Method 动词所表示不了的,就应该把这个动作做成一种资源。
HATEOASRoy Thomas Fielding 在论文中还提到了 HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用状态的引擎)的概念。
所谓 “应用状态”,即:客户端的状态,可以理解为会话状态。服务端以 HyperMedia(超媒体的)形式将资源展示在客户端,当客户端访问其中的超媒体的链接(URI)时,就可以获取该链接关联的资源或者可以对资源执行特定的操作。获取的资源或者经特定操作响应后的资源在经过同样 Resource Request Handler 确定表现层后,继续以超媒体的形式在表现在客户端中。而这种资源内容或形式的改变都会导致客户端会话状态的改变,所以媒体就成为了驱动客户端会话状态转换的引擎(应用状态的改变基于超媒体的改变)。
简而言之,HATEOAS 就是不断的在 Response Body 加入 HyperMedia API 链接,以供客户端进行调用。
通常的,API 的调用者完全掌握了 URI 是怎么设计的。一个解决方法就是在 Response Body 中给出相关链接,便于客户端进行下一步操作的判断。使得用户需要查阅繁琐的文档,也知道下一步应该怎么做。
Github 的 API 就实现了 HATEOAS,访问 api.github.com 会得到一个所有可用 API 的网址列表:
{
"current_user_url": "https://api.github.com/user",
"authorizations_url": "https://api.github.com/authorizations",
// ...
}
从上面可以看到,如果想获取当前用户的信息,应该继续访问 api.github.com/user,然后就得到了下面结果:
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}
服务器的返回中提示了相关文档的网址。
HATEOAS 格式并没有统一的标准,上面例子中,GitHub 就将它们与其他属性放在了一起。其实,更好的做法应该是将相关链接与其他属性分开在不同的区间中:
HTTP/1.1 200 OKContent-Type: application/json
{
"status": "In progress",
"links": {[
{ "rel":"cancel", "method": "delete", "href":"/api/status/12345" } ,
{ "rel":"edit", "method": "put", "href":"/api/status/12345" }
]}
}
OpenStack 也大量的使用到了这种设计:
HTTP/1.1 200 OK
Content-Type: application/json
{"servers": [{
"status": "ACTIVE",
"links": [{
"href": "http://192.168.10.111:8774/v2.1/e5ab2182bb984f3bb4773d4a83672549/servers/95f684d4-0802-484e-b852-7ded35a8eeb5",
"rel": "self"
}, {
"href": "http://192.168.10.111:8774/e5ab2182bb984f3bb4773d4a83672549/servers/95f684d4-0802-484e-b852-7ded35a8eeb5",
"rel": "bookmark"
}],
"image": {
"id": "be4e8e37-226f-4784-b19d-a439400edca0",
"links": [{
"href": "http://192.168.10.201:8774/e5ab2182bb984f3bb4773d4a83672549/images/be4e8e37-226f-4784-b19d-a439400edca0",
"rel": "bookmark"
}]
},
"flavor": {
"id": "ed218eec-1e00-4ea9-93e7-f6e4e7c0ba93",
"links": [{
"href": "http://192.168.10.201:8774/e5ab2182bb984f3bb4773d4a83672549/flavors/ed218eec-1e00-4ea9-93e7-f6e4e7c0ba93",
"rel": "bookmark"
}]
},
"id": "95f684d4-0802-484e-b852-7ded35a8eeb5",
......
}]}