RESTful API 设计实践

作者: caixw
修改时间:

RESTful API 为网络应用程序设计提供了一套统一、合理的风格。它只是一种风格,而不是标准,所以也就没有一套统一的标准去规范化这些设计,本文从实践的角度出发,讨论 RESTful API 设计上的一些细节,探讨如何设计出一套好用、合理、精炼的 API。

版本

按照 RESTful API 的风格,不同版本的 API 应该是同一种资源的不同表现形式,所以将版本号放在报头,是最符合学术界对 RESTful API 的的定义,但是实际操作情况下,将版本号放在报头不直观,而且操作起来也不方便,反而不如直接放在 URL 来得直接。所以现在两种方式都很常见,各人可以按照自己的喜好来挑一种方式来实现。

个人倾向于直接放在报头中,客户在迁移版本时,会更加方便,基本上不用改任何内容,版本交叉使用也方便。

将版本号放在 URL:

  • https://api.caixw.io/v1/users
  • https://api.caixw.io/v2/users

将版本号放在 Accept 报头中:

  • Accept: application/json;version=1
  • Accept: application/json;version=2

路径

在 RESTful 中一条 URL 表示的是一个独立的资源,是该资源的唯一标志。命名上应该具有自描述性,给人一种直觉上的关联,比如 https://api.caixw.io/users/1 让人一看就知道表示的是 ID 为 1 的用户。统一使用名词复数,会使 URL 看起来更加规整,而且对开发者和使用者来说,有个统一的规定,也更容易理解和实现。

  • GET https://api.caixw.io/users 表示用户列表;
  • PUT https://api.caixw.io/users/1 替换 ID 为 1 的用户信息;
  • DELETE https://api.caixw.io/users/1/orders/5 删除 ID 为 1 的用户的第 5 条订单。

一此无法使用 CRUD 表示的操作,应该尽量抽象成相应的操作,比如登录和注销,应该是添加或是删除一个登录的 token:

  • POST https://api.caixw.io/login 登录;
  • DELETE https://api.caixw.io/login 注销。

过滤参数

使用 GET 获取数据时,当数据量过大时,并不是所有数据都是用户需要一次获取的,这时候在服务端对结果进行过滤、排序、分页、查询等功能再返回会是一个比较友好的操作。这些功能应该通过 URL 的查询参数实现。参数值应该尽量避免无意义的值,比如 state=1 从开发者的角度来说,或许可以省略好多的工作量,但是从使用者的角度来说,很难记住这个值 1 代表的是什么意思,当 state 有许多的不同的值时,结果会更加糟糕,所以给出一个确切的字符串来表示:state=lock,会是一个好习惯。

  • GET https://api.caixw.io/users?state=lock 只获取锁定状态的用户列表;
  • GET https://api.caixw.io/users?state=lock&size=20 只获取锁定状态的用户列表,最多显示 20 条记录;
  • GET https://api.caixw.io/users?sort=created,id 按 created 和 id 字段排序;

请求方法

RESTful 的核心思想就是将各个不同的 URL 理解成逻辑上的资源,针对资源做 CRUD 的操作。而这些 CRUD 的操作分别对应着不同的 HTTP 请求方法:

方法 幂等 安全 描述
GET 获取资源(一项或是多项)
POST 在服务器新建一个资源
PUT 替换当前资源(客户需要提供完整的资源属性)
PATCH 修改当前资源部分属性
DELETE 删除当前资源
OPTIONS 获取当前资源所支持的方法列表
HEAD 仅获取报头信息,不包含资源本身的内容。

幂等表示任意多次操作所产生的影响与一次操作产生的影响相同,即使用相同参数重复操作,获取的结果也是相同的。

安全是指该操作是否会对服务器内容作出修改。

当 PUT 所指的资源还不存在时,其功能上和 POST 是极为相似的,唯一的不同是 PUT 预先知道了资源的地址,所以多次操作时,均指向同一地址。而 POST 则会每次操作都添加一条新资源;当 PUT 所指的资源存在时,其功能与 PATCH 相似,都为修改资源内容,只不过 PUT 要求修改资源的所有数据,而 PATCH 只修改资源的部分数据。所以 PUT 在不同的情况下,分别可以扮演 POST 和 PATCH 两种角色。

用户在分析一个 API 时,可能会用到 HEAD 和 OPTIONS 方法,但是现实中,很少有哪些应用是真正去实现这两个方法的,开发者可根据自身情况看是否需要提供该接口。在跨域操作中,浏览器在每访问一个 API 之前,会访问该 API 的 OPTIONS 方法,以确定服务器是否允许该 API 的访问,所以如果你的接口需要跨域,对 OPTIONS 请求方法的处理是必需的。

状态码

完整的状态码可参考 W3C 的相关文档。这里列出几个常有的状态值:

  • 200:GET 服务器成功返回用户请求的数据;
  • 201:POST 用户成功创建一个新的资源;
  • 204:DELETE, PUT 服务器无需返回任何内容;
  • 400:提交的数据不符合要求;
  • 401:登录信息验证不通过;
  • 404:未的到该资源;
  • 500:服务端错误。

错误处理

HTTP 状态码本身就是一套完善的错误描述机制,状态码相当于错误 ID,返回内容相当于错误内容描述。当然对于一个应用系统来,这些有点简单了,所以一般我们都会做一套符合自己需求的错误描述机制,比如 RFC7807 定义了一种错误描述机制:

1{
2    "type": "https://example.com/problems/404100",
3    "title": "用户不存在"
4}

如果有提交数据的,我们还可以指定具体的字段错误信息,这样客户可以很方便地知道具体是哪个字段出错:

1{
2    "type": "https://example.com/problems/404100",
3    "title": "提交的数据有误",
4    "invalid-params": [
5        { "name": "username", "reason": "与已有账号相同" },
6        { "name": "password", "reason": "不能为空" },
7        { "name": "nickname", "reason": "不能为空" }
8    ]
9}
  • type 表示错误代码,同时也可以是一个可访问的 URL,对应的页面给出具体的错误信息,我们可以直接扩展 HTTP 状态码来达到目的。比如我们可以将 401 的状态码进行细化: 401001 表示用户不存在,401002 表示密码错误等;
  • title 对错误信息的简要描述;
  • detail 对错误信息的具体描述;
  • 其它的自定义字段;

content-type 也可以是自定义,比如 json 类型的可以是 application/problem+json

安全

数据安全

我们一般都会用一个自增的 ID 作为资源的唯一 ID,但是如果把这一 ID 直接呈现给用户的话,就有可能不经意间泄露了你的业务信息。比如查看用户订单列表的接口:/users/100/orders,返回以下数据:

1{
2    "count": 2,
3    "orders": [
4        {"id": 100, "name": "商品1", "created": "2017-07-01"},
5        {"id": 1000, "name": "商品1", "created": "2017-07-30"}
6    ]
7}

用语根据当前的订单 ID 就可以获取平台的订单量;用户只要在月初和月末各下一单,还能获取到平台的订单月增长量。对于这类的敏感数据,要使用具有唯一性的随机数据,比如 UUID。

上在的接口定义还有一个问题,即指定多余的用户 ID。100 为当前用户的 ID,有些聪明的人就会把 100 换成其它数据试试,这时候如果你的服务端没有做限定的话,无形就造成了用户数据的泄密。甚至是你做了限定,但是对该 ID 用户是否存在,返回了不同的状态提示,对方也可以根据这些试出用户数量。所以对于限定用户的一些接口,我们可以把其 ID 省去,由服务端根据登录的 token 作判断,接口可以改成:/orders

服务器安全

HTTPS

全站启用 HTTPS 协议,不要在意那么点性能浪费,相对于安全,完全是值得的。

Strict-Transport-Security

HSTS 报头只对 HTTPS 启作用,一旦浏览器接收到这个报头,之后的一段时间之内(时间由 HSTS 报头指 的 max-age 指定)只会对服务器发送 HTTPS 请求,否则就会拒绝传输任何数据。

格式为:Strict-Transport-Security:max-age=17000604;includeSubDomains,其中 includeSubDomains 是一个可选值,指定了表示同时作用于子域名;max-age 指定了时间段,单位为秒,超过该时间段将不再启作用,不过每次获取带有该报头的响应时,都会刷新超始时间。

限定访问频率

限定用户的访问频率,一般是全站所有的接口,在一段时间内,用户有个访问的上限,超过这个上限,我们可以拒绝请求,返回 429 状态码。可以通过令牌桶算法实现。总共包含以下三个报头:

  • X-Rate-Limit-Limit: 同一个时间段所允许的请求的最大数目;
  • X-Rate-Limit-Remaining: 在当前时间段内剩余的请求的数量;
  • X-Rate-Limit-Reset: 为了得到最大请求数所等待的秒数。

其它

  • 将 API 部署到专门的域名下,比如:https://api.example.com。当然如果接口够简单,且不会有扩展的可能,也可以直接放在主域名下:https://example.com/api/
  • 使用 JSON 作为数据交换的格式。JSON 是比 XML 更加轻便的数据交互格式,若没有特殊原因,建议使用 JSON 作为数据交换格式,当然如果你有条件,也可两种格式都提供;
  • 输出格式化之后的数据;
  • 启用内容压缩。

本作品采用署名 4.0 国际 (CC BY 4.0)进行许可。

唯一链接:https://caixw.io/posts/2016/restful-api-guide.html