简介
在主流公司的程序开发中,为了提高程序开发迭代的速度,基本都是前后端分离架构,而前端包括网页、App、小程序等等,因此必须要有一个统一的规范用于约束前后端的通信,RESTful API 则是目前比较成熟的 API 设计理论。
要想理解 RESTful,就需要先明白 REST。REST 是 Roy Thomas Fielding 在其2000年的博士论文中提出的。Fielding 是一个非常重要的人,他是 HTTP 协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。
REST 是RE
presentational S
tate T
ransfer 词组的缩写,可以翻译为"表现层状态转移",其实这个词组前面省略了个主语–“Resource”,加上主语后就是"资源表现层状态转移"。
每个词都能看懂,连在一起就不知道什么意思了?
-
Resource(资源)
所谓资源,就是互联网上的一个实体。URI(Uniform Resource Identifier)的全称就是统一资源标识符,和我们这里的资源一个意思。一个资源可以是一段文字、一张图片、一段音频、一个服务。
-
表现层(Representation)
“资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层”(Representation)。比如一篇文章,可以使用 XML、JSON、HTML 的形式呈现出来;一张图片可以用 JPG 格式表现,也可以用 PNG 格式表现。
-
状态转移(State Transfer)
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化,比如文件被修改,访问数量增加等。互联网通信协议 HTTP 协议,是一个无状态协议,这意味着所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。
上面我们介绍了 REST 的基本概念,那么符合 REST 规范的设计,我们就可以称为 RESTful。
RESTful 架构,是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制,所以正得到越来越多网站的采用。
接下来我们就来看一下 RESTful API 的设计规范。
协议
协议是最基本的设计,表示前后端的通信规范,现阶段应该使用 HTTP/HTTPs 协议。
API 与用户的通信协议,尽量使用 HTTPs 协议。HTTPs 协议的所有信息都是加密传播,第三方无法窃听,具有校验机制,一旦被篡改,通信双方会立刻发现,配备身份证书,防止身份被冒充。
域名
API 的根入口点应尽可能保持足够简单,这里有两个常见的例子:
- http://api.example.com/* (专用域名下)
- http://example.com/api/* (主域名下)
域名应该考虑拓展性,尽量将 API 部署在专用域名之下;如果确定 API 很简单,后续不会拓展,可以考虑放在主域名下。
路径
路径又称为端点(Endpoints),表示 API 的具体网址。在路径的设计中,需遵守下列约定:
- 命名必须全部小写
- URI 必须是名词,并且必须是复数形式
- 如果要使用连字符,使用 “-” 而不是 “_”,因为 “_” 字符可能会在某些浏览器或屏幕中被部分遮挡或完全隐藏
- 易读
命名必须全部小写和易读都无需解释,可以理解为规定,那么为什么命名必须是名词且需要复数形式呢?这是因为在 RESTful 中,主语是资源,资源肯定是名词,不能是动词。其次,一个资源往往对应数据库中一张表,表就是实体的集合,因此需要是复数形式。
下面是一些反例:
- http://api.example.com/getUser
- http://api.example.com/addUser
下面是一些正例:
-
http://api.example.com/zoos
-
http://api.example.com/animals
避免多级URI
常见的情况是,资源需要多级分类,因此很容易写出多级的 URI,比如获取某个作者的某一类文章。
GET /authors/12/categories/2
这种 URI 不利于扩展,语义也不明确,往往要想一会才能明白其含义。
更好的做法是,除了第一级,其他级别都用查询字符串表达。
GET /authors/12?categories=2
下面是另一个例子,查询已发布的文章。你可能会设计成下面的 URI。
GET /articles/published
查询字符串的写法明显更好
GET /articles?published=true
URI结尾不应包含斜杠"/"
这是作为 URI 路径处理中最重要的规则之一,正斜杠 “/” 不会增加语义值,且可能导致混淆。REST API 不允许一个尾部的斜杠,不应该将它包含在提供给客户端的链接的结尾处。
许多 Web 组件和框架将平等对待以下两个URL:
-
http://api.example.com/zoos/
-
http://api.example.com/zoos
但是,实际上 URI 中的每个字符都会计入资源的唯一身份的识别中。
两个不同的 URI 映射到两个不同的资源。如果 URI 不同,那么资源也应该不同,反之亦然。因此,REST API 必须生成和传递精确的 URI,不能容忍任何的客户端尝试不精确的资源定位。
有些 API 碰到这种情况,可能设计为让客户端重定向到相应没有尾斜杠的 URI(也有可能会返回301 - 用来资源重定向)。
HTTP动词
对于如何操作资源,有相应的 HTTP 动词对应,常见的动词有如下五个:
- GET:从服务器取出资源(一项或多项)
- POST:在服务器新建一个资源
- PUT:在服务器更新资源(客户端提供改变后的完整资源)
- PATCH:在服务器更新资源(客户端提供改变的部分资源)
- DELETE:从服务器删除资源
示例:
HTTP动词 | 路径 | 表述 |
---|---|---|
GET | /zoos | 获取所有动物园信息 |
POST | /zoos | 新建一个新的动物园 |
GET | /zoos/ID | 获取指定动物园的信息 |
PUT | /zoos/ID | 更新指定动物园的信息(前端提供该动物园的全部信息) |
PATCH | /zoos/ID | 更新指定动物园的某部分信息(前端提供该动物园改动部分的信息) |
DELETE | /zoos/ID | 删除某个动物园 |
GET | /zoos/ID/animals | 获取某个动物园里面的所有动物信息 |
过滤信息
如果数据量很大,服务器不可能将全部数据都返回给前端,因此前端需要提供一些参数进行过滤,用于分页展示或者排序等,下面是一些常见的参数:
- ?limit=10:指定返回记录的数量
- ?offset=10:指定返回记录的开始位置
- ?page=2&per_page=100:指定第几页,以及每页的记录数
- ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序
- ?animal_type_id=1:指定筛选条件
参数的设计允许存在冗余,即允许 API 路径和 URI 参数偶尔有重复。
HATEOAS
HATEOAS 是 Hypermedia As The Engine Of Application State 的缩写,从字面上理解是"超媒体作为应用状态的引擎" 。其原则就是客户端与服务器的交互完全由 API 动态提供,客户端无需事先了解如何与服务器交互,即返回结果中提供链接,指向其他 API 方法,使用户不查文档,也知道下一步应该做什么。
比如,当用户向 api.example.com 的根目录发出请求,会得到这样一个文档。
{
"link":
{
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/json"
}
}
上面代码表示,文档中有一个 link 属性,用户读取这个属性就知道下一步该调用什么 API 了。
-
rel 表示这个 API 与当前网址的关系(collection 关系,并给出该 collection 的网址)
-
href 表示 API 的路径
-
title 表示 API 的标题
-
type 表示返回类型
Github 的 API 就是这种设计,访问 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://docs.github.com/rest/reference/users#get-the-authenticated-user"
}
上面代码表示,服务器给出了提示信息,以及文档的网址。
版本控制
API 一直保持静态的可能性很小,随着业务需求变化,可能会添加新的资源,底层的数据结构可能也会有更改。在更新 API 提供新功能的同时,需要考虑对已使用该 API 用户的影响,因此需要保持向前兼容,这就引出了版本控制。主要的版本控制方法有如下几种:
URI版本控制
每次修改 Web API 或更改资源的架构时,向每个资源的 URI 添加版本号。 以前存在的 URI 应像以前一样继续运行,并返回符合原始架构的资源。
- http://api.example.com/v1/*
- http://api.example.com/v2/*
该方法的版本控制机制非常简单,但是随着 API 多次迭代,服务器需要支持多个版本的路由,增大了维护的成本。 此方案也增加了 HATEOAS 实现的复杂性,因为所有链接都需要在其 URI 中包括版本号。
查询字符串版本控制
不是提供多个 URI,而是通过追加查询字符串的方式来指定版本,例如
- http://api.example.com/zoos/1?version=2
如果 version 参数被较旧的客户端应用程序省略,则应默认为有意义的值(例如 1)。
此方法具有语义优势(即,同一资源始终从同一 URI 进行检索),但它依赖于代码处理请求以分析查询字符串并发送回相应的 HTTP 响应。 该方法也与 URI 版本控制机制一样,增加了实现 HATEOAS 的复杂性。
自定义请求标头进行版本控制
在请求的 header 中自定义版本控制选项。
GET http://api.example.com/zoos/1 HTTP/1.1
Custom-Header: api-version=2
Accept标头进行版本控制
当客户端应用程序向 Web 服务器发送 HTTP GET 请求时,使用 Accept 标头规定它可以处理的内容的格式。 通常,Accept 标头的用途是客户端指定响应的正文应是 XML、JSON 或者其他可处理的格式。 但是,我们也可以指定该标头为客户端需要的资源版本。
GET http://api.example.com/zoos/1 HTTP/1.1
Accept: application/vnd.example.com.v2+json
上例将 Accept 标头指定为 application/vnd.example.com.v2+json。vnd.example.com.v2 元素向 Web 服务器指示它应返回资源的版本是 v2,而 json 元素则指定响应正文的格式应为 JSON。
其中 vnd
表示 Standards Tree
标准树类型,有三个不同的树: x
,prs
和 vnd
。
你使用的标准树需要取决于你开发的项目
- 未注册的树(
x
)主要表示本地和私有环境 - 私有树(
prs
)主要表示没有商业发布的项目 - 供应商树(
vnd
)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。
此方法可以说是最纯粹的版本控制机制并自然地适用于 HATEOAS。
在现实世界中,API 永远不会完全稳定。因此,如何管理这一变化非常重要。至于具体把版本号放在什么地方,这个问题一直存在很大的争议,对于大多数 API 而言,商定好部分版本的控制策略,然后对 API 详细记录和逐步弃用是可接受的做法。
服务端响应
API 响应,需要遵守 HTTP 设计规范,选择合适的状态码返回。你可能见过有的接口始终返回状态码200,然后通过返回体中的 code 字段进行区分请求是否成功,这种是不符合规范的,相当于状态码没有了任何作用,下面就是一个反例。
HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com
{
"code": -1,
"msg": "该活动不存在"
}
其次,在出现错误时,需要返回错误信息,常见的返回方式就是放在返回体中。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive
{
"error_code": 401,
"message": "Unauthorized"
}
状态码
HTTP 状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,HTTP 状态码共分为5种类型:
分类 | 描述 |
---|---|
1xx | 信息,服务器收到请求,需要请求者继续执行操作 |
2xx | 成功,操作被成功接收并处理 |
3xx | 重定向,需要进一步的操作以完成请求 |
4xx | 客户端错误,请求包含语法错误或无法完成请求 |
5xx | 服务器错误,服务器在处理请求的过程中发生了错误 |
1xx 状态码
API 不需要 1xx 类型的状态码,因此我们主要看下其他几个类型常见的状态码。
2xx 状态码
状态码 | 英文名称 | 描述 |
---|---|---|
200 | OK | 请求成功,一般用于 GET 和 POST 请求 |
201 | Created | 请求成功并创建了新的资源,用于 POST、PUT、PATCH 请求。例如新增用户、修改用户信息等,同时在返回体中,我们既可以返回创建后实体的所有信息数据,也可以不返回相关信息 |
202 | Accepted | 已接受请求,但未处理完成,会在未来再处理,通常用于异步操作 |
204 | No Content | 该状态码表示响应实体不包含任何数据,使用 DELETE 进行删除操作时,需返回该状态码 |
3xx 状态码
API 一般用不到 301 状态码(永久重定向)和 302 状态码(暂时重定向,307 也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。
API 用到的 3xx 状态码,主要是 303 See Other,表示参考另一个 URI。它与 302 和 307 的含义一样,也是"暂时重定向",区别在于 302 和 307 用于 GET 请求,而 303 用于POST、PUT 和 DELETE 请求。收到 303 以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。
下面是一个例子。
HTTP/1.1 303 See Other
Location: /api/orders/12345
4xx 状态码
状态码 | 英文名称 | 描述 |
---|---|---|
400 | Bad Request | 客户端请求的语法错误,服务器无法理解 |
401 | Unauthorized | 表示用户没有权限(令牌、用户名、密码错误) |
403 | Forbidden | 没有权限访问该请求,服务器收到请求但拒绝提供服务 |
404 | Not Found | 服务器无法根据客户端的请求找到资源(如路径不存在) |
405 | Method Not Allowed | 客户端请求的方法服务端不支持,例如使用 POST 方法请求只支持 GET 方法的接口 |
406 | Not Acceptable | 用户 GET 请求的格式不可得(比如用户请求 JSON 格式,但是只有 XML 格式) |
408 | Request Time-out | 客户端请求超时 |
410 | Gone | 客户端 GET 请求的资源已经不存在。410 不同于 404,如果资源以前有现在被永久删除了可使用 410 代码,网站设计人员可通过 301 代码指定资源的新位置 |
415 | Unsupported Media Type | 通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送 |
429 | Too Many Requests | 客户端的请求次数超过限额 |
5xx 状态码
5xx 状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。
状态码 | 英文名称 | 描述 |
---|---|---|
500 | Internal Server Error | 客户端请求有效,服务器处理时发生了意外 |
503 | Service Unavailable | 服务器无法处理请求,一般用于网站维护状态 |
认证和授权
应该使用 OAuth 2.0 的方式为 API 调用者提供登录认证。必须先通过登录接口获取 Access Token
后再通过该 token
调用需要身份认证的 API。
Oauth 的端点设计示列:
- Twitter /oauth2/token
- Fackbook /oauth/access_token
- Google /o/oauth2/token
- Github /login/oauth/access_token
服务端在返回 access token
的同时必须在响应中包含一个名为 expires_in
的数据,它表示当前获得的 token
会在多少秒后失效。
{
"access_token": "token....",
"token_type": "Bearer",
"expires_in": 3600
}
客户端在请求需要认证的 API 时,必须在请求头 Authorization
中带上 access_token
。
Authorization: Bearer token...
当超过指定的秒数后,access token
就会过期,再次用过期/或无效的 token
访问时,服务端应该返回 invalid_token
的错误或 401
错误码。
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"error": "invalid_token"
}
其他
- 服务器返回的数据格式,应该尽量使用 JSON,避免使用 XML
参考
https://juejin.cn/post/7025222833798119454
https://www.ruanyifeng.com/blog/2011/09/restful.html
https://www.ruanyifeng.com/blog/2014/05/restful_api.html
https://segmentfault.com/a/1190000015384373