RESTful规范的API接口设计

在项目中,需要为 APP 撰写 API。刚开始接触的时候,并没有考虑太多,就想提供 URL,APP 端通过该 URL 进行查询、创建、更新等操作即可。但再对相关规范进行了解后,才发现,API 的设计并没有那么简单,远远不是 URL 的问题,而是一个通信协议的整体架构。

使用 SSL(https)来提供 URL

首先,使用 https 可以在数据包被抓取时多一层加密。

我们现在的 APP 使用环境大部分都是在路由器 WIFI 环境下,一旦路由器被入侵,那么黑客可以非常容易的抓取到用户通过路由器传输的数据,如果使用 http 未经加密,那么黑客可以很轻松的获取用户的信息,甚至是账户信息。

其次,即使使用 https,也要在 API 数据传输设计时,正确的采用加密。

例如直接将 token 信息放在 URL 中的做法,即使你使用了 https,黑客仅能抓到域名字符部分,不能抓到请求的数据,但是 URL 可以在浏览器或特殊客户端工具中直接看到。因此,使用 https 进行请求时,要采用 POST、PUT 或者 HEAD 的方式传输必要的数据。

使用 GET、POST、PUT、DELETE 这几种请求模式

请求模式也可以说是动作、数据传输方式,通常我们在 web 中的 form 有 GET、POST 两种。

而在 HTTP 中,存在以下这几种。

请求方式 功能
GET(选择) 从服务器上获取一个具体的资源或者一个资源列表。
POST(创建) 在服务器上创建一个新的资源。
PUT(更新) 以整体的方式更新服务器上的一个资源。
PATCH (更新) 只更新服务器上一个资源的一个属性。
DELETE(删除) 删除服务器上的一个资源。
HEAD 获取一个资源的元数据,如数据的哈希值或最后的更新时间。
OPTIONS 获取客户端能对资源做什么操作的信息。

在 URI 中体现资源,而非动作

阅读 RESTful 架构的参考文献之后,你会了解什么是资源的概念,以及 REST 的确切含义。再构建 API 的 URL 的时候,URI 中应该仅包含资源(对象),而不要加入动作。

比如 /user/1/update ,其中 update 就是一个动作,虽然我们希望通过这个 URI 来实现用户 ID 为 1 的用户进行信息更新,但是按照 RESTful 的规范,update作为动作,应该用上面的 PUT 来表示,所以请求更新用户信息,应该使用 PUT /user/1 来表示更新用户 ID 为 1 的用户信息。

如果去对应上面的请求模式:

  • GET 表示显示、列出、展示
  • POST 表示提交、创建
  • PUT 表示更新
  • DELETE 表示删除

版本

API 的开发直接关系了 APP 是否可以正常使用,如果原本运行正常的 API,突然改动,那么之前使用这个 API 的 APP 可能无法正常运行。APP 是不可能强迫用户主动升级的,因此,通过 API 版本来解决这个问题。也就是说,API 的多个版本是同时运行的,而且都要保证可以正常使用。

按照 RESTful 的规范,不同的版本也应该用相同的 API URL,通过 header 信息来判断版本,再调用不同版本的程序进行处理。但是这明显会给开发带来巨大的成本。

解决办法有两种:

  1. 新版本兼容旧版本,所有旧版本的动作、字段、操作,都在新版本中可以被实现,但明显这样的维护成本很大;
  2. 不同的版本,用不同的 URL 来提供服务,比如在 URL 中通过 v1、v2 来区分版本号,或者采用子域名的方式,比如 v2.api.xxx.com/user 的方式。

HTTP 响应码

在用户发出请求,服务端对请求进行响应时,给予正确的 HTTP 响应状态码,有利于让客户端正确区分遇到的情况。

状态码 请求方式 描述
200 [GET] OK成功)服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)
201 [POST/PUT/PATCH] CREATED已建立)用户新建或修改数据成功
202 [*] (Accepted接受请求)表示一个请求已经进入后台排队(异步任务)
204 [DELETE] (NO CONTENT无内容) 用户删除数据成功
300 Multiple Choices多种选择) 针对请求,服务器可执行多种操作。服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
301 Permanently Moved永久移动) 请求的网页已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302 Temporarily Moved临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
303 See Other查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码
304 Not Modified未修改) 自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
305 Use Proxy使用代理) 请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理。
307 Temporary Redirect(临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
400 [POST/PUT/PATCH] INVALID REQUEST(坏请求) 用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 [*] Unauthorized (未授权) 表示用户没有权限(令牌、用户名、密码错误)
403 [*] Forbidden (禁止)表示用户得到授权(与401错误相对),但是访问是被禁止的
404 [*] NOT FOUND (未找到)用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
405 Method not allowed(方法不被允许)不支持该Request的方法
406 Not Acceptable(无法接受)无法使用请求的内容特性来响应请求的网页,也就是后台的返回结果前台无法解析(比如用户请求 JSON 格式,但是只有 XML 格式)
407 Proxy Authentication Required(要求进行代理认证)与状态码401类似, 用于需要进行认证的代理服务器.
408 Request Timeout(请求超时) 如果客户端完成请求时花费的时间太长, 服务器可以回送这个状态码并关闭连接
410 [GET] Gone (过去的)用户请求的资源被永久删除,且不会再得到的
422 [POST/PUT/PATCH] Unprocesable entity (不可处理)当创建一个对象时,发生一个验证错误
500 [*] INTERNAL SERVER ERROR(服务器内部错误)服务器发生错误,用户将无法判断发出的请求是否成功

返回值结构

在完成了上面的 URL 部署之后,接下来我们来看看返回结果应该怎么样来确定。

我看到大部分文献中指出,最好使用 JSON 进行返回,而非 xml。

我认为原因可能有两点:

  1. JSON 可以很好的被很多程序支持,javascript 的 ajax 可以直接将 JSON 转换为对象。
  2. JSON 的格式在容量上比 xml 小很多,可以减低宽带占用,提高传输效率。

那么,返回值应该怎么去部署呢?

首先,字段的合理返回,数据的包裹。因为返回值中,我们常常要对数据进行区分分组,或者按照从属关系打包,所以,我们再返回时,最好有包裹的思想,把数据存放在不同的包裹中进行返回。

{
    'error_code': 0,
    'data': {
        'user_id': 1,
        'username': 'xiaomin'
    },
    'server_time': 14939939
}

上面返回的 JSON 中,使用 data 来作为数据包,将所有数据统一以这个字段进行包裹。除了 data,也可以用 list 等其他形式的包裹,命名都是自己来根据自己的需要确定的。

{
    'error_code': 0,
    'list': [
        {'user_id': 1, 'username': 'xiaoming'},
        {'user_id': 2, 'username': 'goudan'}
    ]
    'server_time': 14939939
}

总之,不要不分包,直接把所有数据和一些你想返回的全局数据混在一起进行返回。

其次,错误码。错误码的作用是方便查找错误原因,通常情况下,可以用 error_code 来表示,当 error_code=0 时,表示没有发生错误,当 error_code>0 时,发生了错误,并且提供较为详细的文档,告诉客户端对应的 error_code 值所产生的错误的原因和位置。

最后,空白压缩和字符转换。也就是返回的 JSON 结果不要换行和空格,用一行返回结果,使整个结果文本容量最小。同时,中文等字符或结果中有引号,都进行字符转换,防止结果无法被正确识别。

鉴权

其实也就是客户端的权限控制。一般而言,会给客户端分发一个 token 来确定该客户端的唯一身份。客户端在请求时,通过这个 token,判断发出请求的客户端所对应的用户,及其相关信息和权限。

前文已经提到了,token 信息不是用来进行处理的数据,虽然可以通过 POST、PUT 等进行数据提交或传输,但是从 RESTful 规范来讲,它不属于操作数据,在服务端进行处理时,仅是利用 token 进行鉴权处理,所以,我的建议是通过 header 来发送 token。