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 请求方法:

方法 幂等[1] 安全[2] 描述
GET 获取资源(一项或是多项)
POST 在服务器新建一个资源
PUT 替换当前资源(客户需要提供完整的资源属性)
PATCH 修改当前资源部分属性
DELETE 删除当前资源
OPTIONS 获取当前资源所支持的方法列表
HEAD 仅获取报头信息,不包含资源本身的内容。
[1] 幂等表示任意多次操作所产生的影响与一次操作产生的影响相同,即使用相同参数重复操作,获取的结果也是相同的。
[2] 安全是指该操作是否会对服务器内容作出修改。

当 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,返回内容相当于错误内容描述。当然对于一个应用系统来,这些有点简单了,所以一般我们都会做一套符合自己需求的错误描述机制,市面上比较常见的方法就是返回一个固定的数据结构来描述具体的错误内容,比如:

{
    "id": 40101,
    "message": "用户不存在"
}

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

{
    "id": 40001,
    "message": "提交的数据有误",
    "detail": {
        "username": "与已有账号相同",
        "password": "不能为空",
        "nickname": "不能为空"
    }
}

关于错误代码,我们可以直接扩展 HTTP 状态码来达到目的。比如我们可以将 401 的状态码进行细化: 401001 表示用户不存在,401002 表示密码错误,401003 表示 token 过期等。

安全

数据安全

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

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

用语根据当前的订单 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 状态码。可以通过令牌桶算法实现。总共包含以下三个报头:

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

其它

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