Road to growth of rookie

Meaningful life is called life

0%

API 设计小谈

When you encounter a problem, you need to solve the problem, not to escape, and there is no way to escape, you still have to solve it in the end

关于 API 的设计, 一直都是一个比较难搞的问题, 大家对标准也有不同的理解. 这几年里我也接触了好几个项目, 有自己独立设计的, 也有辅助同事设计的. 虽然都是一些中小型的项目 (最大的一个 QPS 1400), 但是开发的过程中也学到了很多东西

有一点我认为尤其的重要, 在多人开发中, 如果大家拟定了一个规范, 不管这个规范是好或者是坏 (前提是: 这个方案是经过讨论经过 大多数人同意 的), 请一定要准守. 这样会让其他开发人员在理解你的代码逻辑的时候, 能更容易看懂; 一旦有人开始不准守规范的时候, 那么项目维护和开发会非常的蛋疼

RESTful 风格的 API

用还是不用 RESTful , 在我们的团队里经过了很多次很多次的讨论, 我们的第一个项目所有的请求都使用 POST 的方式提交, 返回的状态码也都一致 (成功就是: 200, 失败就是: 500); 这种方式存在很大的问题, 例如: 在排查请求日志时, 只能通过 URL 去筛选, 一旦想知道用户执行了多少更新操作, 多少删除操作就会很麻烦. 当然还有很多问题, 使用过的朋友应该都知道

当我们接触到 RESTful 这个概念时, 当时的想法是: 哇喔~, 这是个啥, 这个好牛逼!!. 之后的几个项目, 都把 API 的风格都切换到了 RESTful, 当然这个过程并不是那么的友好, 一个新概念的使用, 中间的试错成本还是挺大的

当然 RESTful 也有它自己的缺点, 这里我就不列举了

为什么在上面说, 在开发过程中一定要准守团队的规范

最近一段时间公司发展比较迅速, 人员流动也比较大, 开发任务又急, 很多新的同事来不及对项目有一定了解, 就要开始开发任务. 所以出现了很多 个人风格 的代码, 也出现了很多违背规范的代码, 导致整个项目看起来很乱, 需要花费大量的时间, 去梳理和重构. 在梳理和重构的过程中, 就对是否需要继续使用 RESTful 发起了争论

其中争论最大的一点, 就是项目中引入了 请求和响应加密 的需求, 在 RESTful 中资源的唯一标识 (例如: /users/1) 都是放在 URL 上的, 但是需求并不想向外部暴露任何信息

项目中 API 设计的一些实践

客户端环境数据和一些参数的收集

一般的项目里, 对客户端的环境, 多多少少会有一些依赖, 例如: 系统、包名等; 客户端的原生请求头 User-Agent 会带上一些数据, 有些是我们需要的, 有些是我们不需要的, 不够简洁明了, 最终我们对 User-Agent 做了一定的修改, 最终的格式如下:

1
package/1.0.0 iOS/11.2.6(iphone x)
  • package App 的包名, 很多软件都会有 马甲包, package 就是用于区分 马甲包
  • 1.0.0 软件版本
  • iOS 客户端操作系统, 一般就是 iOS/Android
  • 11.2.6 客户端操作系统版本
  • iphone x 客户端设备型号
敏感数据不要使用 id 作为唯一标识符

在最初的几个项目中, 我们对安全这一块都没有太多的关注, 其实, 在 web 开发来说 安全 是一个尤为重要的问题.

因为对安全没有太多的关注, 所以在安全上也吃了几个不小的亏, 例如: 被人 DDOS, 被人刷数据等等.

不使用 id 作为敏感数据的唯一标识, 是因为, id 在数据库中一般都是一个 自增且连续 的数字, 当被恶意用户发现这个规律时 (这个规律太容易发现了), 可能会通过轮询恶意获取到这部分数据, 所以敏感数据的唯一标识, 一定要是无序的

对于数据, 没有敏感不敏感, 任何一部分数据对于公司来说都是有价值的

JWT 的一些实践

一般的 JWT 分为三个部分: HeaderPayloadSignature, 我们使用的是魔改之后的 JWT, 一共只有两个部分: UUIDPayload, 其中 UUID 是用户的唯一标识, Payload 是经过加密的用户数据

为什么要使用 UUID 代替 Header?

传统的 JWTHeader 是一个元数据的 json 对象, 然后通过 Base64URL 算法转成的字符串. 之所以要使用 UUID 代替它, 最主要 的是因为需要通过用户的唯一标识, 定位到用户的所有请求日志, 原生的 JWT 是没有办法通过用户部分信息定位到日志的

Paylaod 中包含的就是:

1
2
3
4
5
6
{
"id": 1,
"uuid": 10020203,
"expired_at": 1631091571,
"updated_at": 1631091571
}
  • exppired_at: token 过期时间
  • updated_at: token 刷新过期时间, 一般是 exppired_at + 一周
请求和响应的加解密

上面有说过, 在我们是否需要继续使用 RESTful 扽讨论中, 争论最大的一点, 就是关于 URL 参数的问题上. 最后的讨论的结果, 是继续使用 RESTful, 但是客户端的请求全部都是 POST 发送, 在服务端的业务代码之前, 起一个 openresty 服务, 用 lua 写一个脚本, 去 拆解和转发 客户端的请求, 并对请求和响应加解密

例如客户端修改用户信息接口 PUT /users/1 就变成了:

1
2
3
4
5
6
7
8
9
{
"path": "/users/1",
"method": "PUT",
"query": "name=a&age=2",
"payload": {
"nickname": "new nickname",
"age": 18
}
}

客户端所有的请求都通过一个统一的 URL, 例如: POST /api, openresty 收到请求之后, 会根据请求的 payload 拆解成一个标准的 RESTful 风格的格式并转发到业务机器上

版本化

一个项目, 经常会发生变化。业务变化可能修改 API 参数或响应数据结构, 以及资源之间的关系. 一般来说, 字段的增加不会影响旧的客户端运行. 但是当存在一些破坏性修改时, 就需要使用新的版本将数据导向到新的资源地址.

注意: 这里说的版本化是服务端接口版本化, 和客户端的软件版本没有关系

比较推荐的做法是使用 URL 前缀,例如 /v1/users/ 表达获取 v1 版本下的用户列表