WebAPI规范设计——违RESTful

廉价感情. 提交于 2020-02-04 16:01:22

本文首先简单介绍了几种API设计风格(RPC、REST、GraphQL),然后根据实现项目经验提出WebAPI规范设计思路,一些地方明显违反了RESTful风格,供大家参考!

一、几种设计风格介绍

1.1 RPC

这是最常见的方式,RPC说的是本地调用远程的方法,面向的是过程,估计超过50%的API是这种分格的。

  • RPC形式的API组织形态是类和方法,或者说领域和行为。
  • 因此API的命名往往是一个动词,比如GetUserInfo,CreateUser
  • 因为URI会非常多而且往往没有一些约定规范,所以需要有详细的文档。
  • 也是因为无拘无束,HTTP方法基本只用GET和POST,设计起来比较简单。

1.2 REST

REST架构风格有四个级别的成熟度:

  • 级别 0:定义一个 URI,所有操作是对此 URI 发出的 POST 请求。
  • 级别 1:为各个资源单独创建 URI。
  • 级别 2:使用 HTTP 方法来定义对资源执行的操作。
  • 级别 3:使用超媒体(HATEOAS)。

级别0其实就是类RPC的风格,级别3是真正的REST,大多数号称REST的API在级别2。REST实现一些要点包括:

  • REST形式的API组织形态是资源和实体,一切围绕资源(级别1的要点)。
  • 会定义一些标准方法(级别2的要点),然后把标准方法映射到实现(比如HTTP Method): 

    • GET:获取资源详情或资源列表。对于collection类型的URI(比如/customers)就是获取资源列表,对于item类型的URI(比如/customers/1)就是获取一个资源。
    • POST:创建资源,请求体是新资源的内容。往往POST是用于为集合新增资源。
    • PUT:创建或修改资源,请求体是新资源的内容。往往PUT用于单个资源的新增或修改。实现上必须幂等。
    • PATCH:部分修改资源,请求体是修改的那部分内容。PUT一般要求提交整个资源进行修改,而PATCH用于修改部分内容(比如某个属性)。
    • DELETE:移除资源。和GET一样,对于collection类型的URI(比如/customers)就是删除所有资源,对于item类型的URI(比如/customers/1)就是删除一个资源。
  • 需要考虑资源之间的导航(级别3的要点,比如使用HATEOAS,HATEOAS是Hypertext as the Engine of Application State的缩写)。有了资源导航,客户端甚至可能不需要参阅文档就可以找到更多对自己有用的资源,不过HATEOAS没有固定的标准。

对于一套设计精良的REST API,其实客户端只要知道可用资源清单,往往就可以轻易根据约定俗成的规范以及导航探索出大部分API。比较讽刺的是,有很多网站给前端和客户端的接口是REST的,爬虫开发者可以轻易探索到所有接口,甚至一些内部接口,毕竟猜一下REST的接口比RPC的接口容易的多。

1.3 GraphQL

如果说RPC面向过程,REST面向资源,那么GraphQL就是面向数据查询了。“GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。”

二、风格选择

  • 在下列情况考虑RPC风格的API或说是RPC:
    • 偏向内部的API
    • 没有太多的时间考虑API的设计或没有架构师
    • 提供的API很难进行资源、对象抽象
    • 对性能有高要求
  • 在下列情况考虑REST风格:
    • 偏向外部API
    • 提供的API天生围绕资源、对象、管理展开
    • 不能耦合客户端实现
    • 资源的CRUD是可以对齐的(功能完整的)
  • 在下列情况考虑GraphQL:
    • 客户端对于数据的需求多变
    • 数据具有图的特点
  • 在下列情况考虑服务端驱动:
    • 客户端发版更新困难,需要极端的灵活性控制客户端
    • 仅限私有API

三、违RESTful规范设计

3.1 文档系统

无论是独立的wiki还是整合在网关系统中,文档系统都应该支持全局模糊搜索。文档有3种,全局设计规范、API参考手册、文档系统本身的使用指南。其中,参考手册的每个API说明应包括这些信息:

  • URL
  • method
  • 传参方式(URL query、HTTP header、body)
  • 请求与响应的参数表
    • 参数名
    • 类型(string、array、object、int、float、bool)
    • 是否必填,必填时的默认值
    • 说明:中文名称、功能意义、取值范围。至少要能看出对应需求文档或UI稿的哪个东西
  • 可能的错误码与错误信息
  • 示例

3.2 全局规范

URL格式

  • 大小写和连接符的规范应该全局统一:snake_case或者camelCase。一般snake_case会更像普通的web URL。如果使用RESTful,URL格式可以是https://api.example.com/{service_name}/{version}/{api_name}?{filters}https://www.example.com/api/{service_name}/{version}/{api_name}?{filters}。注:Web URL和API URL的规范是不同的。
  • 简单查询尽量用GET,好处是可以直接带查询参数copy api路径。
  • 复杂查询和更新用POST,用的最多。
  • 不用PUT和DELETE,原因是增加复杂度,并没有带来什么好处。看看BAT的很多openapi,也是写着restful,实际没有严格遵守,都是get和post,这是也很多人不知道put和delete的原因。
//根据订单id获取订单
GET api/order/queryOrderById?id=value1&param2=value2 
//根据订单id List获取订单
POST api/order/queryOrderByIdList 
//根据条件查询订单,带分页参数
POST api/order/queryOrderByCondition 
//更新订单收款状态
POST api/order/updateOrderCollectionStatus 
//批量更新订单收款状态
POST api/order/updateOrderCollectionStatusInBatch 
//批量更新订单收款状态
POST api/order/updateOrderCollectionStatusInBatch 
//批量删除订单,带操作来源
POST api/order/deleteOrderInBatch

如果不用RESTful,最好也不要把参数放到header里,尽量在HTTP协议框架内实现业务。在此前提下,如果存在(所有接口都需要的)公共参数,可以放在URL query里;或者最方便地使所有接口的method都是POST然后放body里。
JSON格式的Content-Typeapplication/json,如果进行了加密和BASE64转码,则应该是text/plain。如有必要,可进一步指定编码:application/json;charset=utf-8

业务参数 以下讨论的是放在body里的JSON

  • 各个key的大小写和连接符的规范应该全局统一:snake_case或者camelCase。
  • 必填参数应该约定默认值。如不指定,可认为各类型的默认值是0、0.0、{}、[]、""、false,决不能是null或undefined
  • 非必填参数在无值时有3种风格,应该选定一种全局统一:

    • 不存在这个key
    • value是null
    • value是undefined
  • 非必填参数不允许是这个参数类型的默认值(0、0.0、{}、[]、""、false
  • 值是数字就用数字类型,不要用字符串。
  • 布尔类型用JSON的语义true/false来表示,不要用1/0
  • 值为枚举时,尽量用字符串表示而不是用数字。牺牲一点点性能但可以大大增强代码可读性。这能大幅降幅维护成本,减少出错。
// 直接把枚举value写成字符串更便于开发维护
type: "duck",
type: "chicken"
// “用数字表示然后在文档中详细说明”可读性差,通常不会有人把文档复制成代码注释
type: 1,  // duck
type: 2   // chicken
  • 尽量不要为前端做格式化。例如时间,应返回“1970年1月1日0点至今的秒数”或者“按ISO8601进行格式化的UTC(世界标准时间)时间”,而不是直接返回“2018年11月11日 23:22:33”。让大前端自己做格式化能更好应对UI变化以及兼容特殊要求。比如客户端从中文切换成英文,界面上的“5月6日”需要变成“May 6th”;这种场景下如果是后端传的“5月6日”,那无论是再次请求英文还是客户端自行解释时间后做转换都是糟糕的设计。
  • 保证向后兼容的前提下及时删除废弃的参数或接口。可以先对参数或接口标记Deprecated,在前端发布后或客户端强制升级后删除。
  • 同一意义的字段名,在不同接口返回的命名统一。不要这边叫“page_count”,那边叫“page_size”或"page_amount"。

响应体

较常见的JSON结构是这样的:

{
  "status": 0,
  "message": "",
  "data": {}
}
  • status:0表示正常/成功,非0代表错误码。
  • message:表示错误信息。
  • data:业务数据。所有的业务信息都应该放到data对象上。data一定是对象

其中,错误码和错误信息也可以设计一份全局统一的对照表。需要注意的是,这里的status都表示业务情况,跟HTTP的status不要混用。 各级网关都可能以HTTP status表示错误,故它无法明确表示是业务API的问题。简单的例子是,业务API鉴权失败,HTTP也应该返回200 OK而不是返回401。因为接口是正常的,是数据逻辑不正确。

如果不用考虑多语言,message错误信息可以是面向用户的中文语句,由前端/客户端直接toast告知用户。

分页设计

  • 请求参数应包含:
    1. 当前页码。接口文档应注释是从0还是从1开始计数。
    2. 每页条数
    3. (可选)排序
  • 响应参数应包含:
    1. (可选)当前页码
    2. (可选)每页条数
    3. (可选)排序
    4. 数据数组
    5. 总页数或总条数,或是否还有下一页。总之不要让使用者再请求一次才知道没有更多数据了。

组合请求

为了减少请求数,后端可提供组合请求接口,并且可组合任意接口。假如有3个接口(示例的响应体经简化仅保留data):

/a:请求{"a": "a"}会响应{"data": {"d": "d"}}
/b:请求{"b": "b"}会响应{"data": {"e": "e"}}
/c:请求{"c": "c"}会响应{"data": {"f": "f"}}

增加一个接口/combo可以一次性获取这3个接口的数据:

// 请求
{
  "api": {
    "/a": {"a": "a"},
    "/b": {"b": "b"},
    "/c": {"c": "c"}
  }
}
// 响应
{
  "data": {
    "/a": {"data": {"d": "d"}},
    "/b": {"data": {"e": "e"}},
    "/c": {"data": {"f": "f"}}
  }
}

防攻击

  • 加密机制。可对body做加密,使用AES、DES、3DES、RSA、DSA、ECC等算法。一般会对密文做BASE64转码再在网络上传输。
  • 校验机制,防篡改。对body做签名,可使用MD5、SHA1、HMAC等算法。签名只能放在URL query或HTTP header中。
  • 防重放机制。可使用timestamp、nonce等机制,在一定时间内重复即认为是重放。或者timestamp距今超过一定时间的,认为是非法请求。

可参考资料

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!