Rest API - 规范

自己定义的Rest API规范.

最后更新:2019-01-01

什么是REST API

一组REST API被建模为一组可独立寻址的资源(API的名词)。资源被通过资源名称被引用,通过一小组方法(也被称为动词或者操作)被操作。

1. 通用说明

REST API数据交互格式为JSON,要求请求参数进行UTF-8编码。所有的请求必须包含请求头: Content-Type:application/json;charset=utf-8

为了保证格式统一,所有的参数、返回属性都采用驼峰表示。此项可以根据具体实现改为下划线

为避免调用方与服务端时区不一致导致的时间不一致问题,所有的时间都采用unix时间戳存储,均为long类型。unix时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。调用方可以根据自己的时区将时间转换为合适的时间显示。

文档中采用{}包含的文字表示变量,需要根据实际环境修改。

所有的接口不支持重复的参数,例如传入limit=1&limit=10时,接口会按照limit=1处理,忽略掉limit=10。此项为建议项,不强制要求。

~对于POST和PUT请求,一定要有请求体。如果在文档中POST和PUT请求没有说明参数,表示该接口不需要参数,但是一定要使用一个空的map(或json){}作为参数。~此项由API网关的特殊性决定,不强制要求

所有未说明类型的参数和返回值,均默认为string

所有对外发布的REST API建议都使用https访问,禁止HTTP访问。建议使用权威机构发布的SSL证书。只对内发布的REST API不需要支持HTTPS访问,但需要在防火墙上禁止外部调用。

2. URI格式

URL是URI的一个子集(一种具体实现),对于REST API来说一个资源一般对应一个唯一的URI(URL)。在URI的设计中,我们会遵循一些规则,使接口看起透明易读,方便使用者调用。

2.1. “/”分隔符

”/”分隔符一般用来对资源层级的划分,例如

http://api.example.com/shapes/polygons/quadrilaterals/squares

对于REST API来说,”/”只是一个分隔符,并无其他含义。为了避免混淆,”/”不应该出现在URL的末尾。例如以下两个地址实际表示的都是同一个资源:

http://api.example.com/shapes/  
http://api.example.com/shapes

2.2. “-“连接符

URI中尽量使用连字符”-“代替下划线”_“的使用。 连字符”-“一般用来分割URI中出现的字符串(单词),来提高URI的可读性,例如:

http://api.example.com/blogs/mark-masse/entries/this-is-my-first-post

使用下划线”_“来分割字符串(单词)可能会和链接的样式冲突重叠,而影响阅读性。

但实际上,”-“和”“对URL中字符串的分割语意上还是有些差异的:”-“分割的字符串(单词)一般各自都具有独立的含义,可参见上面的例子。而”“一般用于对一个整体含义的字符串做了层级的分割,方便阅读,例如你想在URL中体现一个ip地址的信息:210_110_25_88 .

补充说明:“-“叫做分词符,顾名思义用作分开不同词的。这个最佳实践来自于针对Google为首的SEO(搜索引擎优化)需要,Google搜索引擎会把url中出现的”-“当做空格对待,这样url”/it-is-crazy” 会被搜索引擎识别为与“it”,”is”,”crazy”关键词或者他们的组合关键字相关。当用户搜索”it”,”crazy”, “it iscrazy”时,很容易检索到这个url。

2.3. 大小写

URI中统一使用小写字母

根据RFC3986定义,URI是对大小写敏感的,所以为了避免歧义,我们尽量用小写字符。但主机名(Host)和scheme(协议名称:http/ftp/…)对大小写是不敏感的。

3. HTTP动词

3.1. HTTP动词说明

  • GET(SELECT):从服务器取出资源(一项或多项),完成请求后返回状态码 200 OK;完成请求后需要返回被请求的资源详细信息
  • POST(CREATE):在服务器创建一个资源,创建完成后返回状态码 201 Created;完成请求后需要返回被创建的资源详细信息
  • PUT(UPDATE):用于完整的替换资源或者创建指定身份的资源,比如创建 id 为 123 的某个资源, 如果是创建了资源,则返回 201 Created;如果是替换了资源,则返回 202 Accepted;完成请求后需要返回被修改的资源详细信息.对于没有修改属性的请求,请确保Content-Length的请求头设置为0
  • PATCH(UPDATE):使用部分JSON数据更新资源的部分属性。例如,发行资源具有标题和主体属性。补丁请求可以接受一个或多个属性来更新资源
  • DELETE(DELETE):从服务器删除某个资源。完成请求后返回状态码 204 No Content
  • HEAD:用于只获取请求某个资源返回的头信息
  • OPTIONS:用于获取资源支持的所有 HTTP 方法

3.2. 系统约束

RESTful的核心思想就是,客户端发出的数据+操作指令都是“动词+宾语”的结构,比如GET /articles这个命令,GET是动词,/articles是宾语。

所有的API尽量按照下面的约束来定义动词,对应CRUD操作,动词一律大写:

  • GET:从服务器取出资源(一项或多项),返回状态码 200 OK,返回被请求的资源详细信息
  • POST:在服务器创建一个资源,返回状态码 201 Created,返回被创建资源的主键 result:主键,或者是资源的详细信息
  • PUT:更新资源的部分属性,返回状态码 202 Accepted,返回结果result:1,或者是资源的详细信息
  • DELETE:从服务器删除某个资源。返回状态码 202 Accepted,返回结果result:1。一旦资源被删除,GET方法访问被删除的资源时,要返回404。
  • 其他动词不推荐使用

在设计 Restful API 时,还应该遵循 HTTP 关于 “安全性” 和 “幂等性” 的要求。

  • 安全性:无论请求多少次,都不会改变资源的状态。比如 GET 操作,无论执行多少遍,都不会改变资源的状态。所以对于 GET 类 API,在编写应用端代码时,切记要尽可能避免出现删除或者更新数据的逻辑。
  • 幂等性:无论是执行一次,还是执行多次,效果是等价的(即多个相同的请求和一个请求的副作用相同),比如 DELETE,PUT 操作。以 DELETE 操作为例,删一次和删多次,效果都是该资源不存在了。注意,幂等只针对服务器端的副作用,与任何响应无关。特别要注意的是,DELETE 不存在的资源 应该 返回404 (Not Found)。

在 HTTP 协议,不同 Method 在 “是否有 request body” 和 “是否有 response body” 有所差异。约定如下表:

Method 安全性 幂等性 是否有 request body 是否有 response body
POST
DELETE 最好无
PUT Option
GET

HTTP的幂等意味着,多个相同的请求和一个请求的副作用相同。GET, PUT, 和 DELETE 都是幂等的HTTP方法。注意,幂等只针对服务器端的副作用,与任何响应无关。特别要注意的是,DELETE 不存在的资源 应该 返回404 (Not Found)。

3.3. 动词覆盖

有些客户端只能使用GET和POST这两种方法。服务器必须接受POST模拟其他三个方法(PUT、PATCH、DELETE)。这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override属性,告诉服务器应该使用哪一个动词,覆盖POST方法。

POST /api/Person/4 HTTP/1.1  
X-HTTP-Method-Override: PUT

5. 响应码

客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。HTTP 状态码就是一个三位数,分成五个类别。

  • 1xx:相关信息
  • 2xx:操作成功
  • 3xx:重定向
  • 4xx:客户端错误
  • 5xx:服务器错误

这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

API 不需要1xx状态码,下面介绍其他四类状态码的精确含义。

5.1. 请求成功

  • 200 OK 请求执行成功并返回相应数据,如 GET 成功
  • 201 Created 对象创建成功并返回相应资源数据,如 POST 成功;创建完成后响应头中应该携带头标 Location ,指向新建资源的地址
  • 202 Accepted : 接受请求,但无法立即完成创建行为,比如其中涉及到一个需要花费若干小时才能完成的任务。返回的实体中应该包含当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便客户端能够获取最新状态
  • **204 No Content **: 请求执行成功,不返回相应资源数据,如 PATCH , DELETE 成功

不强制请求成果一定要返回201,202,204的状态

5.2. 重定向

重定向的新地址都需要在响应头 Location 中返回

  • 301 Moved Permanently 被请求的资源已永久移动到新位置
  • 302 Found 请求的资源现在临时从不同的 URI 响应请求
  • 303 See Other 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该使用 GET 方法进行请求。不推荐使用,此代码在HTTP1.1协议中被303/307替代。我们目前对302的使用和最初HTTP1.0定义的语意是有出入的,应该只有在GET/HEAD方法下,客户端才能根据Location执行自动跳转,而我们目前的客户端基本上是不会判断原请求方法的,无条件的执行临时重定向
  • 307 Temporary Redirect 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该保持原有的请求方法进行请求

API 用不到301状态码(永久重定向)和302状态码(暂时重定向,307也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。

API 用到的3xx状态码,主要是303 See Other,表示参考另一个 URL。它与302和307的含义一样,也是”暂时重定向”,区别在于302和307用于GET请求,而303用于POST、PUT和DELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子:

HTTP/1.1 303 See Other
Location: /api/orders/12345

5.3. 条件请求

  • 304 Not Modified 资源自从上次请求后没有再次发生变化,不会返回资源的消息实体,主要使用场景在于实现数据缓存,用来减轻服务端压力
  • 409 Conflict 请求操作和资源的当前状态存在冲突。主要使用场景在于实现并发控制
  • 412 Precondition Failed 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。主要使用场景在于实现并发控制

5.4. 客户端错误

  • 400 Bad Request 服务器不理解客户端的请求,未做任何处理,用于客户端一般性错误返回, 在其它4xx错误以外的错误,也可以使用400,具体错误信息可以放在body中
  • 401 Unauthorized 需要验证用户身份,如果服务器就算是身份验证后也不允许客户访问资源,应该响应 403 Forbidden。如果请求里有 Authorization 头,那么必须返回一个 WWW-Authenticate 头
  • 403 Forbidden一般用于非验证性资源访问被禁止,例如对于某些客户端只开放部分API的访问权限,而另外一些API可能无法访问时,可以给予403状态
  • 404 Not Found 找不到目标资源
  • 405 Method Not Allowed 不允许执行HTTP方法,响应中应该带有 Allow 头,内容为对该资源有效的 HTTP 方法
  • 406 Not Acceptable 服务器不支持客户端请求的内容格式,但响应里会包含服务端能够给出的格式的数据,并在 Content-Type 中声明格式名称
  • 410 Gone 被请求的资源已被删除,只有在确定了这种情况是永久性的时候才可以使用,否则建议使用 404 Not Found
  • 413 Payload Too Large POST 或者 PUT 请求的消息实体过大
  • 415 Unsupported Media Type 服务器不支持请求中提交的数据的格式
  • 422 Unprocessable Entity 请求格式正确,但是由于含有语义错误,无法响应
  • 428 Precondition Required 要求先决条件,如果想要请求能成功必须满足一些预设的条件

5.5. 服务端错误

  • 500 Internal Server Error 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。
  • 501 Not Implemented 服务器不支持当前请求所需要的某个功能。
  • 502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
  • 503 Service Unavailable 由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间(内容可以为数字,单位为秒;或者是一个 HTTP 协议指定的时间格式)。如果没有给出这个 Retry-After 信息,那么客户端应当以处理 500 响应的方式处理它。

501 与 405 的区别是:405 是表示服务端不允许客户端这么做,501 是表示客户端或许可以这么做,但服务端还没有实现这个功能

5.6. 发生错误时,不要返回 200 状态码

有一种不恰当的做法是,即使发生错误,也返回200状态码,把错误信息放在数据体里面,就像下面这样

HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "failure",
  "data": {
    "error": "Expected at least two items in list."
  }
}

上面代码中,解析数据体以后,才能得知操作失败。

这张做法实际上取消了状态码,这是完全不可取的。正确的做法是,状态码反映发生的错误,具体的错误信息放在数据体里面返回。下面是一个例子。

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "Invalid payoad.",
  "detail": {
     "surname": "This field is required."
  }
}

6. 资源对象的CRUD

资源对象是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/articles这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的。

  • /getAllCars
  • /createNewCar
  • /deleteAllRedCars

既然 URL 是名词,那么应该使用复数,还是单数?这没有统一的规定,但是常见的操作是读取一个集合,比如GET /users(读取所有用户),这里明显应该是复数。 为了统一起见,建议都使用复数 URL,比如GET /users/2要好于GET /user/2

GET /users            获取user列表
GET /users/:id        根据id获取单个user
POST /users           创建user
PUT /users/:id        根据id更新user
DELETE /users/:id     根据id删除user

聚合资源必须通过父级资源操作,如: Profile是User的聚合资源,User有一个唯一且私有的Profile资源,只能通过User操作Profile。

GET /users/:id/profile        根据用户id获取profile
POST /users/:id/profile       为用户创建profile
PUT /users/:id/profile        根据用户id更新profile
DELETE /users/:id/profile     根据用户id删除profile

组合资源要避免资源路径嵌套,如: 一个系统里面包含多个 applications,一个 application 又包含多个 users。那获取 user 资源的路径不应该使用嵌套,它会让你的接口变得越来越混乱和缺少灵活性:

GET /systems/:systemId/applications/:applicationId/users/:userId

正确的做法是:

GET /systems/:systemId
GET /applications/:applicationId 
GET /users/:userId/

6.1. 不符合 CRUD 的情况

在实际资源操作中,总会有一些不符合 CRUD(Create-Read-Update-Delete) 的情况,一般有几种处理方法。

  • 使用 POST

为需要的动作增加一个 endpoint,使用 POST 来执行动作,比如 POST /resend 重新发送邮件。

  • 增加控制参数

添加动作相关的参数,通过修改参数来控制动作。比如一个博客网站,会有把写好的文章“发布”的功能,可以用上面的 POST /articles/{:id}/publish 方法,也可以在文章中增加 published:boolean 字段,发布的时候就是更新该字段 PUT /articles/{:id}?published=true

  • 把动作转换成资源

把动作转换成可以执行 CRUD 操作的资源, github 就是用了这种方法。

比如“喜欢”一个 gist,就增加一个 /gists/:id/star 子资源,然后对其进行操作:“喜欢”使用 PUT /gists/:id/star,“取消喜欢”使用 DELETE /gists/:id/star

另外一个例子是 Fork,这也是一个动作,但是在 gist 下面增加 forks资源,就能把动作变成 CRUD 兼容的:POST /gists/:id/forks 可以执行用户 fork 的动作。

例如:

GET /articles/id/like:查看文章是否被点赞

PUT /articles/id/like:点赞文章

DELETE /articles/id/like:取消点赞

7. 通用请求头

调用方必须包含下列HTTP请求头:

Content-Type:application/json;charset=utf-8

8. 通用响应头

服务端的响应必须包含下列HTTP响应头:

x-request-id 服务端生成的跟踪ID
x-response-time 服务端处理耗时,单位毫秒
x-server-time 服务端响应时间,取服务器时间,需要带上时区,例如2017-04-27T18:19:28+08:00

9. 统计信息

API一般都需要记录用户的隐私数据,比如手机型号、系统版本等数据,约定使用请求头x-user-analysis传递,内容为下列JSON字符串的使用约定的加密算法加密后的字符串

  • osName 系统名称,如IOS,Android
  • osVersion 系统版本,如5.1
  • model 手机型号,如IPHONE 6
  • appName 应用名称
  • appVersion 应用版本号
  • deviceId 设备ID
  • 其他自定义属性

也可以通过User-Agent约定

注意:在项目中我们一般会使用友盟、极光等厂商提供的方案,而不是自己实现这个功能

10. 安全认证

如果API需要安全认证,调用方需要在请求头加上Authorization: Bearer (登录后获取的token值)。 如果API服务需要实现安全认证功能,建议使用[JWT](https://edgar615.github.io/jwt.html) 实现。

11. 日期显示

日期我们要求基于Unix时间戳实现,但是在实际开发中会对调用方产生一些困扰,增加了调用方日期时间和时间戳转换的工作量,因此对于日期时间我们采用下面的规则:

  1. 接口文档使用日期时间字符串,由接口实现方处理日期时间字符串和Unix时间戳之间的转换
  2. 如果项目仅针对国内用户,按照一般的用户习惯,日期时间格式为2017-04-27 10:58:30
  3. 如果项目需要支持全球用户,日期时间格式建议采用 ISO_8601 建议的格式,接口提供方要避免调用方与服务端时区不一致导致的时间不一致的问题

ISO_8601:年由4位数字组成YYYY,月、日用两位数字表示:MM、DD。只使用数字为基本格式。使用短横线”-“间隔开年、月、日为扩展格式。

下面我们可以了解一下 ISO_8601 建议的格式,从wikipedia中复制

11.1. 日期

年为4位数,月为2位数,月中的日为2位数,例如,日期(2017年4月27日)可表示为**2017-04-27 **

11.2. 时间

只使用数字为基本格式。使用冒号”:”间隔开小时、分、秒的为扩展格式。小时、分和秒都用2位数表示。 对于当地时间15时27分46秒,表示为15:27:46

对协调世界时的时间最后加一个大写字母Z,如UTC时间下午2点30分5秒表示为14:30:05Z

其他时区用实际时间加时差表示,当时的UTC+8时间表示为22:30:05+08:00或223005+0800,也可以简化成223005+08

11.3. 日期和时间的组合

日期和时间合并表示时,要在时间前面加一大写字母T,如要表示北京时间2004年5月3日下午5点30分8秒,可以写成2004-05-03T17:30:08+08:00

11.4. 时间段

如果要表示某一作为一段时间间隔,前面加一大写字母P,但时间段后都要加上相应的代表时间的大写字母。如在一年三个月五天六小时七分三十秒内,可以写成P1Y3M5DT6H7M30S

11.5. 时间间隔

从一个时间开始到另一个时间结束,或者从一个时间开始持续一个时间间隔,要在前后两个时间(或时间间隔)之间放置斜线符”/”。格式为:

<start>/<end>
<start>/<duration>
<duration>/<end>
<duration>

例如1985-04-12/1986-01-011985-04-12/P6M, 2007-03-01T13:00:00Z/2008-05-11T15:30:00Z

11.6. 循环时间

前面加上一大写字母R,格式为:

R【循环次数】【/开始时间】/时间间隔【/结束时间】

如要从2004年5月6日北京时间下午1点起时间间隔半年零5天3小时循环,且循环3次,可以表示为R3/2004-05-06T13:00:00+08:00/P0Y6M5DT3H0M0S

如以1年2个月为循环间隔,无限次循环,最后循环终止于2025年1月1日,可表示为R/P1Y2M/20250101

12. API版本

系统在发展的过程中,不可避免的需要添加新的API,或者修改现有API。一旦API已经发布,那么对API的任何改动都需要考虑对当前用户的影响,因此建议API服务使用API版本号来实现API的版本控制策略。

版本控制在业内有三种做法:

  • 1.在URI中直接标记使用的是哪个版本,无版本号URI默认使用最新版本。
https://api.example.com/v1/api
https://api.example.com/api
  • 2.在每个请求后添加一个version参数,表示请求的是哪个版本。
https://api.example.com/api?version=1
  • 3.在HTTP请求的header中使用Media Type标记使用的是哪个版本, 无版本号表示最新版本的API。
curl https://example.com/api/lists/3 \  
-H ' x-api-version: v1' (自定义的Header)
curl https://example.com/api/lists/3 \  
-H 'Accept: application/vnd.example.v2+json' (vendor MIME media type)

根据上面的API版本策略,在参考了一些开放平台的API版本后,我们采用下面的方式进行版本控制。

将API的的版本号分为两种

  • 主版本(Major Version):大版本更新,对业务改动较大,无法满足对调用方的向下兼容,强制调用方升级,因此需要修改版本号。版本号只能使用整数,并放入URL中
https://api.example.com/v1
https://api.example.com/v2
  • 小版本(Minor Version):小版本号,对于业务改动较小,可以在业务逻辑里按版本号进行区分。版本号只能是yyyyMMdd的日期格式,可以将版本号放入 header中
curl https://example.com/api/lists/3 \  
-H ' x-api-version: 20171011'

注意:对API的历史版本支持一定要有时间和用户限制,老版本的API支持到一定时间就删除,新用户必须使用新版API,否则一个API多个版本会大大增加平台的维护工作量。

13. 签名校验

如果API服务需要实现请求数据签名校验功能,按照下面的方式实现。

服务方向调用方发放两个值:

  • appKey:唯一值。每个key表示一个调用方。
  • appSecret:密钥。调用方需妥善保管该值,防止泄密。

调用方在请求API时,需要增加下列参数

  • appKey string 服务端为每个第三方应用分配的appKey
  • nonce string调用方生成的随机数,每次请求都应该不同,主要保证签名不可预测
  • signMethod string签名的摘要算法,可选值MD5、HMACSHA256、HMACSHA512、 HMACMD5
  • sign string 根据签名算法生成的签名,规则后面描述

签名生成的步骤如下:

  1. 将请求方法(GET 或 POST)加上换行符(\n)得到字符串stringA,如GET\n,请求方法请保持大写
  2. 将API路径加上换行符(\n)得到字符串stringB,如果API路径为空,使用正斜杠(/),如/\n
  3. 将查询字符串内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringC。如果请求带请求体,对请求体中的内容计算MD5值,然后按照content-md5=md5值的格式加入到URL键值中,拼接成字符串stringC。查询字符串需要使用 UTF-8 字符形式,参数值应进行了 URL 编码(十六进制字符必须大写如/编码为%2F,不能使用%2f)。参数值内允许有空格,但空格必须经 URL 编码成 %20。
  4. 将stringA、stringB、stringC拼接成新的字符串stringD,如GET\n/\nkey1=value1&key2=value2
  5. 对stringD按照signMethod中指定的算法进行加密得到最终的signValue。如果是MD5加密,需要在stringD的首尾加上appSecret。
  6. 将sign=signValue追加到查询字符串的后面,向服务端发送请求。

下面通过两个例子介绍下sign的生成方式

13.1. 示例1:GET请求 查询安防记录的接口

GET /alarms?type=21&alarmTimeStart=1469280456&alarmTimeEnd=1471958856&start=0&limit=20
  1. 增加通用参数
    /alarms?type=21&alarmTimeStart=1469280456&alarmTimeEnd=1471958856&start=0&limit=20&appKey=XXXXX&nonce=123456&signMethod=HMACMD5
    
  2. 将所有的参数排序得到新的查询字符串
    alarmTimeEnd=1471958856&alarmTimeStart=1469280456&appKey=XXXXX&limit=20&nonce=123456&signMethod=HMACMD5&start=0&type=21
    
  3. 将请求方法、API路径和查询字符串评价成新的字符串
    GET\n/alarm\nalarmTimeEnd=1471958856&alarmTimeStart=1469280456&appKey=XXXXX&limit=20&nonce=123456&signMethod=HMACMD5&start=0&type=21
    
  4. 将上一步得到的字符串使用HMACMD5加密,得到签名7B686C90ACE0193430774F4BE096F128,并追加到查询参数之后
    alarmTimeEnd=1471958856&alarmTimeStart=1469280456& appKey=XXXXX&limit=20&nonce=123456&signMethod=HMACMD5&start=0&type=21&sign=7B686C90ACE0193430774F4BE096F128
    

    5.将上一步得到的新查询字符串加入到接口中调用

    /alarms? alarmTimeEnd=1471958856&alarmTimeStart=1469280456& appKey=XXXXX&limit=20&nonce=123456&signMethod=HMACMD5&start=0&type=21&sign= 7B686C90ACE0193430774F4BE096F128
    

13.2. 示例2:POST请求 用户登录

POST /login
{"username":"foo","password":"bar"}
  1. 增加通用参数
    /login?appKey=XXXXX&nonce=123456&signMethod=HMACMD5
    
  2. 将请求体转换为JSON字符串后追加到参数列表中
    appKey=XXXXX&nonce=123456&signMethod=HMACMD5&content-md5=4746b3b2c0d9e0debe0fb0bfad553052
    
  3. 将所有的参数排序得到新的查询字符串
    ppKey=XXXXX& content-md5=4746b3b2c0d9e0debe0fb0bfad553052&nonce=123456&signMethod=HMACMD5
    
  4. 将请求方法、API路径和查询字符串评价成新的字符串
    POST\n/login\nappKey=XXXXX&content-md5=4746b3b2c0d9e0debe0fb0bfad553052&nonce=123456&signMethod=HMACMD5
    
  5. 将上一步得到的查询字符串使用HMACMD5加密,得到签名A61C44F04361DE0530F4EF2E363C4A45,并追加到查询参数之后,并删除查询字符串中的content-md5
    appKey=XXXXX&nonce=123456&signMethod=HMACMD5&sign= A61C44F04361DE0530F4EF2E363C4A45
    
  6. 将上一步得到的查询字符串加入到接口中调用
    /login?appKey=XXXXX&nonce=123456&signMethod=HMACMD5&sign= A61C44F04361DE0530F4EF2E363C4A45
    

14. 限流

如果API服务支持限流,那么在响应头中必须带上下面的响应头

X-Rate-Limit-Limit - The number of allowed requests in the current period
X-Rate-Limit-Remaining - The number of remaining requests in the current period
X-Rate-Limit-Reset - The number of seconds left in the current period

不要用时间戳来表示X-Rate-Limit-Reset,因为时间戳包含各种有用但不必要的信息,例如日期和时区。API调用方只是想知道什么时候他们可以再次发送请求,使用秒数来回答这个问题,可以让调用方以最小的代价来处理。它也避免了时钟歪斜的问题。

15. 参数

许多API都有一些可选参数。对于GET和DELETE请求,在路径中未包含的参数都可以作为HTTP查询字符串参数传递

curl -i "https://api.example.com/repos/vmg/redcarpet/issues?state=closed"

对于POST和PUT请求,URL中不包含的参数应该被编码为JSON,通过请求体传递,请求头的Content-Type 应该指定为 ‘application/json;charset=utf-8’:

curl -i -u username -d '{"scopes":["public_repo"]}' https://api. example.com/authorizations

在接口中需要使用一些通用参数建议按照下面中的约束来命名。

15.1. 分页

当接口需要提供分页功能时使用下面的参数,并且限制返回的数量。API服务需要根据实际的数据量设计分页查询的实现,避免MySQL的count,offset引发的性能问题。

  • page: int 请求的页码,默认为1
  • perPage: int 请求每页的数量,默认为10,建议范围为整数范围[1,100]

15.2. 限制数量

列表查询的接口一般都需要限制返回的数量,避免返回列表过多。与分页相似,API服务需要根据实际的数据量限制记录的起始索引,避免MySQL的offset引发的性能问题。

  • start: int 指定返回记录的起始索引位置, 默认为1
  • limit: int 返回的记录条数int默认为10,建议范围为整数范围[1,100]

15.3. 限制返回字段

调用方其实并不是总是需要资源的全部信息。在接口中增加可以选择返回哪些字段的参数可以让调用方减少网络流量,并提高API的响应。

  • fields: string 返回的字段,多个字段用逗号”,”分隔

示例:

GET /tickets?fields=id,subject,customerName,updatedAt

15.4. 过滤

如果需要快速从资源列表中过滤资源,可以将需要实现过滤的字段作为查询参数传递,例如 GET /tickets?state=open state就是一个实现了过滤条件的查询参数

15.5. 排序

如果API需要对调用方提供排序功能,使用sort的查询参数来描述排序规则,多个排序规则使用逗号”,”分隔,每个排序规则都可以使用一个”-”表示降序的排序顺序,例如

GET /tickets?sort=priority 按priority升序
GET /tickets?sort=-priority 按priority降序
GET /tickets?sort=-priority,created_at按priority降序,created_at升序

注意:API服务需要检查调用方传入的排序参数是否合法,或者自动删除错误的参数,避免查询出错。

15.6. 总数

对于列表查询,API服务默认不会返回资源的总数。单数如果调用方需要得到资源的数量,API服务可以使用count的查询参数来判断是否给调用方返回资源数量。

  • count: bool 是否返回资源总数 , 默认为false

count=true表示返回资源数量,资源的数量将会通过”Total-Count”的响应头显示,而响应体的内容可能是全部资源的部分数据,例如

GET /api/v1/tickets?count=true	
200 OK
Total-Count: 135
[
  ... results ... 
]

15.7. 搜索

过滤仅能按照简单的方式来对资源进行筛选,但如果调用方需要查询state不等于open的资源时,就无法通过过滤参数得到想要的结果。API服务可以通过查询参数q来定义更复杂的搜索条件。q是由一组特定格式的字符串组成,具体的格式定义如下:

  • foo:bar foo=bar的条件
  • foo:”bar” foo=”bar”的条件
  • stars:>10 stars > 10的条件
  • created:>=2012-04-30 created >=2012-04-30的条件
  • created:>2012-04-29 created >2012-04-29的条件
  • stars:10..* stars >= 10的条件
  • created:2012-04-30..* created >=2012-04-30的条件
  • stars:10..50 stars >=10且stars<=50的条件
  • created:2012-04-30..2012-07-04 created >=2012-04-30且created<=2012-07-04的条件
  • stars:<10 stars < 10的条件
  • stars:<=9 stars <=9的条件
  • created:<2012-07-05 created < 2012-07-05的条件
  • created:<=2012-07-04 created <= 2012-07-04的条件
  • stars:* ..10 stars <= 10的条件
  • created:*..2012-04-30 created <= 2012-04-30的条件
  • foo:*bar 以bar结尾的条件
  • foo:bar* 以bar开头的条件
  • foo:bar 包含bar的条件,慎用不会走索引
  • -language:javascript 取language !=javascript的条件
  • -created:<=2012-07-04 表示created>2012-07-04

对于IN,NOT IN这种查询由于目前需求较小,暂未定义。

foo:bar、foo:bar、foo:bar暂时不支持取反

多个条件用空格组合,URL编码后为+,一个参数名和:之间不要有空格,

网上看到另一种做法,觉得也可以

We can have as many operators as needed such as [lte], [gte], [exists], [regex], [before], and [after]

GET /items?price[gte]=10&price[lte]=100

示例:

后续补充

15.8. 格式化输出

API服务默认输出JSON格式应该尽量减少空格和换行,这样有时候肉眼查看结果时不太友好,API服务可以为调用方提供一个pretty的参数来判断是否要对JSON输出做格式化操作。

  • pretty: bool 是否开启格式化 , 默认为false

此功能由API网关支持,下游服务不需要支持

16. 返回值

16.1. null属性

在restful api开发中, 对于返回的json数据, 如果属性值为null,有两种处理方式

选项1

{
    "name":"bob",
    "age": null
}

选项2

{
    "name":"bob"
}

在一般情况下,我们选择方案1,将null的属性直接返回,因为如果接口越公开, 可能的使用者越多, 那么接口越应该保持不变

  • 如果有个字段,有时候返回, 有时候却不返回, 这样的接口稳定性是比较差的
  • 在说明文档缺失的情况下, 使用者会产生困惑.然后不得不寻找接口的开发人员咨询,提高了接口支撑成本.
  • 如果使用者大多数时候能看到字段, 他会以为字段一直会出现, 然后编写代码的时候可能就不会去判断key是否存在. 当key偶尔不存在导致程序报错时又不好发现和排查.(调用方经常会出现这个问题)
  • 数据key一会出现一会不出现, 会引入不确定性: 如果没有出现, 是因为api改变了吗? 还是因为使用者解析出错了? 还是调用出错了?等等
  • 对于开发接口的人来说: 他得写代码把key=null的数据剔除掉(可以使用jackson工具), 但是毕竟引入了新的逻辑. 新逻辑意味着可能的新风险.

在大多数应用中, 网络延迟才是主要考虑因素, 而不是带宽. 出于性能的考虑, 很多api开发人员更喜欢少量”大接口”,而不是大量”小接口”, 一次拿到很多数据,总体上比不停地调api好一些. 对于大多数应用来说, 数据不一致比存在多余数据更危险.

当然也有一些情况我们需要选择方案2

  • 有一些应用会频繁调用api, 对数据大小分毫必争, 多一个字段代价都很大,那么毋庸置疑,为null的字段不返回更好
  • 有的接口有100个字段, 但是大多数时候90个字段都是null,这种情况下90个字段都无意义的,此时选择精简节约型也更好一些.
  • 部分响应是由类似的东西触发的?fields=foo,bar,其中为所有其他字段返回空值似乎有点违反直觉

16.2. 分页

如果是分页请求,按照下面表格中的约束来命名

  • records JSON数组,记录的列表
  • pageint 当前页码
  • pageSizeint 每页的数量
  • totalRecordsint 总记录数
  • totalPagesint总页数
  • nextPageint下一页页码
  • prevPageint上一页页码

注意:在数据量很大时,不建议totalRecords和totalPages使用真实的值,因为那样可能会引起MySQL的性能问题。

16.3. 总数

对于使用count=true的列表查询,资源的数量将会通过”Total-Count”的响应头显示,而响应体的内容可能是全部资源的部分数据,例如

Total-Count: 135
[
  ... results ... 
]

17. 错误码

错误码是一组数字(或字母与数字的结合),它会与错误讯息建立关联,用于识别在系统中出现的各种异常情况。错误码能为我们带来下列帮助:

  • 通过错误码我们能识别出系统到底出了什么问题
  • 通过错误码我们应当能识别出哪个系统出了问题
  • 通过错误码我们可以决策出该给客户显示出了什么问题

为了达到上述的目的,我们需要对错误码的命名及使用做一定的约束

在公司的规范中,需要为每一条产品线都分配了对应的错误码区间,并且不仅不同的产品线需要划分各自的错误码区间,每条产品线内部的系统,也应当划分各自的区间,这样可以有效地避免出现重复错误码。

目前约定999-9999的错误码为公共错误码,下面的表格是已经使用了的公共错误码,其他产品线/系统的错误码,需要在系统设计之初完成划分

下面是系统已经定义的错误码

  • 999 未知错误 响应码500
  • 1000空指针异常 响应码400
  • 1001未知用户 响应码401
  • 1002未登录用户 响应码401
  • 1003用户名或密码错误 响应码400
  • 1004权限不足 响应码403
  • 1005Token过期 响应码401
  • 1006资源不存在 响应码404
  • 1007对象不存在 响应码400
  • 1008参数不全 响应码400
  • 1009参数非法 响应码400
  • 1010输入为空或字数不够 响应码400
  • 1011输入的字数过多 响应码400
  • 1012文件为空 响应码400
  • 1013上传文件失败 响应码400
  • 1014用户名重复 响应码400
  • 1015请求超时 响应码400
  • 1016未找到远程服务器响应码503
  • 1017参数类型错误 响应码400
  • 1018SQL错误 响应码400
  • 1019类型错误 响应码400
  • 1020服务调用异常 响应码400
  • 1021Token无效 响应码401
  • 1022非法请求 响应码400
  • 1023过期的请求 响应码400
  • 1024非法的JSON格式 响应码400
  • 1025并发冲突 响应码409
  • 1026服务到期 响应码403
  • 1027账号被锁定 响应码403
  • 1028缺少必要的头信息 响应码428

请求如果出现异常或其他错误,返回格式为{"message":"参数非法","code":400}的结果,其中code表示错误码,message表示错误描述。如果错误码还包含一些额外的描述,会返回格式为{"message":"参数非法","foo":"bar","code":1009}的结果,例如1009参数非法的错误码通常会有details的属性

{
  "message": "参数非法",
   "details": {
      "password": [
        "NotEmpty:不能为空"
      ],
      "username": [
        "NotEmpty:不能为空"
      ]
    },
  "code": 1009
}

错误码中的message仅为参考值,在实际业务中可能需要与调用方一起实现message的自定义功能,原因如下:

  1. 因为项目需要支持全球用户访问,错误描述统一定义为英文,但不同地区部署的项目需要使用相应的错误提示
  2. 有些错误描述由开发人员定义,文字描述不太严谨,在线上需要由运营或产品统一调整错误描述

18. 缓存

后续补充

Caching

HTTP provides a built-in caching framework! All you have to do is include some additional outbound response headers and do a little validation when you receive some inbound request headers.

There are 2 approaches: ETag and Last-Modified

ETag: When generating a response, include a HTTP header ETag containing a hash or checksum of the representation. This value should change whenever the output representation changes. Now, if an inbound HTTP requests contains a If-None-Match header with a matching ETag value, the API should return a 304 Not Modified status code instead of the output representation of the resource.

Last-Modified: This basically works like to ETag, except that it uses timestamps. The response header Last-Modified contains a timestamp in RFC 1123 format which is validated against If-Modified-Since. Note that the HTTP spec has had 3 different acceptable date formats and the server should be prepared to accept any one of them.

19. 服务端约束

  • 尽可能保证接口幂等性,不论是读还是写
  • 可以使用乐观锁或者时间戳保证,为了避免时序错乱导致的错误,更新的内容尽量使用增量数据而不是全量数据。
  • 尽可能支持降级
  • 向下兼容
  • 接口要保证无状态

20. 参考资料

https://developer.github.com/v3

http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api

https://github.com/bolasblack/http-api-guide#user-content-http-%E5%8D%8F%E8%AE%AE

http://www.infoq.com/cn/news/2017/09/How-versioning-API

http://www.lexicalscope.com/blog/2012/03/12/how-are-rest-apis-versioned

http://stackoverflow.com/questions/389169/best-practices-for-api-versioning

https://www.moesif.com/blog/categories/api-product-management/

Edgar

Edgar
一个略懂Java的小菜比