卓越API设计:简洁统一开放规范
1 概述
卓越的产品源自精心设计,而API设计则是构建高效交互的基石。它不仅是技术的桥梁,更是理解、使用与集成的导航图,确保各方顺畅协作。API协议,如同精密的契约,定义了服务器与客户端间的交互规则,让内外部利益相关者明了职责与协作之道,共同编织出色的API生态。通过持续优化表达,我们确保API设计既直观又强大,推动产品不断迈向卓越。
本规范的三个目标:简洁、统一、开放。
1.1 什么是接口?
接口全称是应用程序编程接口,是应用程序重要的组成部分。接口可以是一个功能,例如天气查询,短信群发等,接口也可以是一个模块,例如登录验证。接口通过发送请求参数至接口url,经过后台代码处理后,返回所需的结果。
1.2 为什么需要编写接口文档?
由于接口所包含的内容比较细,在项目中常常需要使用接口文档。研发人员可以根据接口文档进行开发、协作,测试人员可以根据接口文档进行测试,系统也需要参照接口文档进行维护等。
1.3 我们采用的接口规范-RESTful API
REST,即Representational State Transfer的缩写。我对这个词组的翻译是"表现层状态转化"。REST的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是"资源"(Resources)的"表现层"。
-
每一个URI代表一种资源;
-
客户端和服务器之间,传递这种资源的某种表现层;
-
客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
2 数据规范
2.1 基本规范
2.1.1 规范原则
●接口返回数据即显示:前端仅做渲染逻辑处理;
●渲染逻辑禁止跨多个接口调用;
●前端关注交互、渲染逻辑,尽量避免业务逻辑处理的出现;
●请求响应传输数据格式:JSON,JSON数据尽量简单轻量,避免多级JSON的出现;
●响应数据名称走遵循小驼峰命名格式。
●接口路径以 /api
或 /[version]/api
开头
正确:/api/task
或 /v2/api/tasks
错误:/biz/tasks
或 /biz/api/tasks
注意:一个产品无论后端有多少个服务组成也应该只有一个 API 入口
●接口路径以 api/aa-bb/cc-dd
方式命名
正确:/api/task-groups
错误:/api/taskGroups
●接口路径使用资源名词而非动词,动作应由 HTTP Method 体现,资源组可以进行逻辑嵌套
正确:POST /api/tasks
错误:POST /api/create-task
●接口路径中的资源使用复数而非单数
正确:/api/tasks
错误:/api/task
●接口设计面向开放接口,而非单纯前端业务
要求我们在给结构路径命名时候面向通用业务的开放接口,而非单纯前端业务,以获取筛选表单中的任务字段下拉选项为例
正确:/api/tasks
错误:/api/task-select-options
虽然这个接口暂时只用在表单的下拉选择中,但是需要考虑的是在未来可能会被用在任意场景,因此应以更通用方式提供接口交由客户端自由组合
2.1.2 请求公共参数
公共参数是每个接口都要携带的参数,描述每个接口的基本信息,用于统计或其他用途,放在header或url参数中。例如:
字段名称 | 说明 |
appid | 服务商应用唯一标识 |
appkey | 服务商应用密钥 |
timestamp | 时间戳 |
sign | 请求签名 |
token | 系统调用的唯一凭证。使用参数???来获取token,token可以设置一次有效(这样安全性更高),也可以设置时效性,这里推荐设置时效性。如果一次有效的话这个接口的请求频率可能会很高 |
2.1.3 规范使用 HTTP 方法
方法 | 场景 | 例如 |
GET | 获取数据 | 获取单个:GET /api/tasks 获取列表:GET /api/tasks |
POST | 创建数据 | 创建单个:POST /api/tasks |
PATCH | 差量修改数据 | 修改单个:PATCH /api/tasks |
PUT | 全量修改数据 | 修改单个:PUT /api/tasks |
DELETE | 删除数据 | 删除单个:DELETE /api/tasks |
有些客户端只能使用GET
和POST
这两种方法。服务器必须接受POST
模拟其他三个方法(PUT
、PATCH
、DELETE
)。
这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override
属性,告诉服务器应该使用哪一个动词,覆盖POST
方法。
上面代码中,X-HTTP-Method-Override
指定本次请求的方法是PUT
,而不是POST
。
2.2 响应状态码规范
{ code: 200, message: "success", data: { } }
code : 请求处理状态
200: 请求处理成功 500: 请求处理失败 401: 请求未认证,跳转登录页 406: 请求未授权,跳转未授权提示页
message: 请求处理消息
code=200 且 data.message="success": 请求处理成功 code=200 且 data.message!="success": 请求处理成功, 普通消息提示:message内容 code=500: 请求处理失败,警告消息提示:message内容
HTTP响应状态码
态码 | 场景 |
200 | 创建成功,通常用在同步操作时 |
202 | 创建成功,通常用在异步操作时,表示请求已接受,但是还没有处理完成 |
400 | 参数错误,通常用在表单参数错误 |
401 | 授权错误,通常用在 Token 缺失或失效,注意 401 会触发前端跳转到登录页 |
403 | 操作被拒绝,通常发生在权限不足时,注意此时务必带上详细错误信息 |
404 | 没有找到对象,通常发生在使用错误的 id 查询详情 |
500 | 服务器错误 |
其它更多响应状态码请查阅 MDN Web Docs
2.3 统一响应数据格式
{
code: 200,
message: "success",
data: {
}
}
为了方便给客户端响应,响应数据会包含三个属性:
●状态码(code)
●信息描述(message)
●响应数据(data)
客户端根据状态码及信息描述可快速知道接口,如果状态码返回成功,再开始处理数据。
array类型数据。通过list字段,保证data的Object结构。
分页类型数据。返回总条数,用于判断是否可以加载更多。
2.3.1 响应实体格式
{
code: 200,
message: "success",
data: {
id: 1,
name: "XXX",
code: "XXX"
}
}
2.3.2 响应列表格式
data.list: 响应返回的列表数据,如果list是多个将list名称转换为实体数组名称如bookList,studentList
{
code: 200,
message: "success",
data: {
list: [
{
id: 1,
name: "XXX",
code: "XXX"
},
{
id: 2,
name: "XXX",
code: "XXX"
}
]
}
}
2.3.3 响应分页格式
{
code: 200,
message: "success",
data: {
totalCount: 2,
totalPage: 1,
pageNo: 1,
pageSize: 10,
list: [
{
id: 1,
name: "XXX",
code: "H001"
},
{
id: 2,
name: "XXX",
code: "H001"
}
]
}
}
data.totalCount: 总记录数 data.pageNo: 当前页码 data.pageSize: 每页大小 data.totalPage: 总页数
2.4 特殊内容规范
2.4.1 Boolean类型
关于Boolean类型,JSON数据传输中一律使用1/0来标示,1为是/true,0为否/false;
Boolean类型,1是0否。客户端处理时,非1都是false。
if("1".equals(isVip)){
......
} else {
......
}
2.4.2 日期类型
关于日期类型,JSON数据传输中一律使用字符串,具体日期格式因业务而定;
2.4.3 上传/下载
上传/下载,参数增加文件md5,用于完整性校验(传输过程可能丢失数据)。
2.4.4 时间字段以 ISO 8601 格式返回 :YYYY-MM-DDTHH:MM:SSZ
2.4.5 空数组使用 [],而不是 null
// 正确
{
code: 200,
message: "请求成功",
data: {
id: 1,
roleIds: []
}
}
// 错误
{
code: 200,
message: "请求成功",
data: {
id: 1,
roleIds: null
}
}
2.4.6前后端传输过程以标准 JSON 格式,避免反复正反序列化
// 正确
{
code: 200,
message: "请求成功",
data: {
roleList: [
{
id: 1,
name: '角色 1'
},
{
id: 2,
name: '角色 2'
}
]
}
}
// 错误
{
code: 200,
message: "请求成功",
data: {
roleList: '[{"id":1,"name":"角色 1"},{"id":2,"name":"角色 2"}]'
}
}
2. 5 创建类接口
2.5.1 创建完成后直接返回 id
{
code: 200,
status: 200,
message: "创建成功",
data: {
id: 1
}
}
2.5.2 关联关系只以 id 为标识,其它字段不应依赖客户端,
以创建用户为例:POST /api/users
// 正确
{
username: 'ming'
password: 'xxxx',
roleIdList: [1, 2, 3]
}
// 错误
{
username: 'ming'
password: 'xxxx',
roleIdList: [
{ id: 1, name: '角色1' },
{ id: 2, name: '角色2' },
{ id: 3, name: '角色3' }
]
}
2.5.3 参数错误以数组形式返回,并附带用户友好的提示
{
code: 400
message: "参数错误",
data: {
errors: [
{
field: 'name',
message: '缺失'
}
]
}
}
2.6 查询类接口
2.6.1 排序使用 sort 和 order
例如 GET /api/tasks?sort=createdTime&order=desc
表示以创建时间降序查询数据
注意:其中 order
为 desc
时表示降序,为 asc
时表示升序
2.7 文件类接口
2.7.1 统一提供单文件上传接口(/api/files
),支持上传所有类型文件
// 请求,注意这里是 FormData{ file: File}
// 响应
{ code: 200,
message: "上传成功",
data: {
id: 'bb313c99',
url: '/files/bb313c99.pdf'
name: '合同.pdf' // 原文件的名称
}
}
2.7.2 统一提供多文件上传接口(/api/multiple-files
),支持上传所有类型文件
// 请求,注意这里是 FormData{ files: [File, File]}
// 响应
{ code: 200,
message: "上传成功",
data: [
{
id: 'bb313c99',
url: '/files/bb313c99.pdf'
name: '合同1.pdf' // 原文件的名称
},
{
id: 'bb313c88',
url: '/files/bb313c88.pdf'
name: '合同2.pdf' // 原文件的名称
}
]
}
2.8. 敏感类接口
2.8.1 涉及到用户隐私的应对相关字段做加密处理
// 正确
{ name: '小明',
idNumber: 'U2FsdGVkX1+1fW7OpO/tlPXe4IGA/bXExlhKwIR/spk=',
password: 'U2FsdGVkX1/AnXKSBDbztNBfp4czlZxQ++3jRtNZhY0='
}
// 错误
{
name: '小明',
idNumber: '310000199511159999',
password: 'ming@xxx.com'
}
注意:本规范不约定使用何种加密算法,请视实际场景选择
2.9. 图表类接口
2.9.1 曲线图、柱状图
{
code: 200,
message: "请求成功",
data: {
xAxis: ['2022.04.20','2022.04.21', '2022.04.22']
series: [
{
name: '上海用户',
data: [5000,4000,3000],
color: '#f5f5f5' // 可选,如果加上的话会使用该色值
},
{
name: '成都用户',
data: [3000,4000,5000], // 注意,没有数据时候也要使用 0 填充,和 xAxis 一一对应
color: '#f5f5f5'
}
]
}
}
2.9.2 饼图
{
code: 200,
message: "请求成功",
data: {
series: [
{
name: '上线用户',
value: 1890,
color: '#f5f5f5' // 可选,如果加上的话会使用该色值
},
{
name: '下线用户',
value: 2000,
color: '#f5f5f5'
}
]
}
}
3 安全性
3.1 调用接口的先决条件 - TOKEN
为了确保接口调用的安全性,所有接口在调用前都需要进行身份验证。TOKEN(令牌)是一种常用的身份验证机制,客户端在调用接口前需要携带有效的TOKEN。服务器在接收到请求后,会验证TOKEN的有效性,只有验证通过的请求才会被处理。
3.2 使用POST作为接口请求方式
在调用接口时,为了提高安全性,我们推荐使用POST方式作为请求方式。与GET请求相比,POST请求不会将参数暴露在浏览器URL中,从而减少了敏感信息泄露的风险。同时,POST请求对参数的长度没有限制,可以传输更大的数据量。
方法 | 描述 | 说明 |
GET | 请求指定的页面信息,并返回实体主体 | 安全且幂等获取表示变更时获取表示(缓存)适合查询类的接口使用 |
POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。post请求可能会导致新的资源的建立和已有资源的修改。 | 不安全且不幂等使用服务端管理的(自动产生)的实例号创建资源创建子资源部分更新资源如果没有被修改,则不过更新资源(乐观锁)适合数据提交类的接口使用 |
PUT | 从客户端向服务器传送的数据取代指定的文档内容 | 不安全但幂等用客户端管理的实例号创建一个资源通过替换的方式更新资源如果未被修改,则更新资源(乐观锁)适合更新数据的接口使用 |
DELETE | 请求服务器删除指定内容 | 不安全但幂等删除资源适合删除数据的接口使用 |
注意:虽然POST请求在安全性上相对于GET请求有所提高,但并不能完全替代其他安全措施。因此,在实际应用中,还需要结合其他安全措施来确保接口的安全性。
3.3 客户端IP白名单
IP白名单是一种有效的安全策略,通过限制只有特定IP地址的客户端才能访问接口,从而降低被恶意攻击的风险。设置IP白名单时,需要谨慎选择允许的IP地址,并确保这些地址是可信的。当客户端的IP地址发生变化时,需要及时更新白名单。
推荐使用防火墙规则进行IP白名单设置,因为防火墙通常具有更高的安全性和可靠性。同时,防火墙还可以与其他安全策略相结合,如端口过滤、协议过滤等,进一步增强系统的安全性。
3.4 单个接口针对IP限流
为了维护系统的稳定性并防止恶意攻击,我们需要对单个接口的调用频率进行限制。可以使用Redis等缓存技术来记录每个IP地址对接口的调用次数,并设置合理的过期时长。当某个IP地址的调用次数超过限制时,可以拒绝其后续请求或进行降级处理。
限流策略需要根据实际业务场景进行调整和优化,以确保既能有效防止恶意攻击,又能满足正常业务的需求。
3.5 记录接口请求日志
记录接口请求日志是排查问题和定位异常请求位置的重要手段。可以使用AOP(面向切面编程)等技术来实现全局请求日志的记录。日志中应包含请求的URL、参数、调用时间、调用结果等关键信息。
通过对日志的分析和挖掘,我们可以及时发现潜在的安全问题和性能瓶颈,并采取相应的措施进行改进和优化。
3.6 敏感数据脱敏
在接口调用过程中,涉及到敏感数据时需要进行脱敏处理。脱敏的目的是保护用户隐私和数据安全,防止敏感信息被泄露或滥用。
RSA非对称加密是一种常用的加密方式,具有较高的安全性。但需要注意的是,加密和解密过程可能会消耗一定的计算资源,因此在实际应用中需要权衡安全性和性能之间的关系。除了加密外,还可以采用其他脱敏方式,如数据替换、数据隐藏等,根据具体业务场景选择合适的脱敏策略。
4 幂等性
4.1 什么是幂等性
幂等性是一个在数学与计算机学中均存在的概念。在数学中,某一元运算为幂等时,表示该运算作用在任一元素两次后,其结果与作用一次相同。在计算机编程中,幂等操作指的是可以多次重复执行且每次执行的影响均与一次执行相同的操作。
4.2 什么是接口幂等性
在HTTP/1.1协议中,幂等性被定义为一次或多次请求某一资源应产生相同的结果(网络超时等问题除外)。即,首次请求可能对资源产生副作用,但后续多次请求不会再次对资源产生副作用。这里的副作用指的是不会对结果产生破坏或产生不可预料的结果。
4.3 为什么需要实现幂等性
实现接口幂等性的主要原因包括:
-
防止前端重复提交表单,如因网络波动导致用户重复点击提交按钮。
-
防止用户恶意刷单,如重复投票等。
-
防止HTTP客户端工具因超时重试机制导致的重复请求。
-
防止消息中间件因错误导致的消息重复消费。
幂等性可以确保接口在多次调用时不会产生未知的问题,保证系统的稳定性和一致性。
4.4 引入幂等性后对系统的影响
虽然幂等性可以简化客户端逻辑处理,防止重复提交等操作,但它也增加了服务端的逻辑复杂性和成本。具体影响包括:
-
可能将并行执行的功能改为串行执行,降低执行效率。
-
需要增加额外的业务逻辑来控制幂等性,使业务功能更加复杂。
因此,在引入幂等性时,需要权衡其利弊,根据实际业务场景具体分析。
4.5 Restful API 接口的幂等性
HTTP Method | 资源操作 | CRUD操作 | 安全性 | 幂等性 | 解释 |
GET | SELECT | SELECT | 安全 | 幂等 | 读操作安全,查询一次多次结果一致 |
POST | INSERT | CREATE | 非安全 | 非幂等 | 写操作非安全,每多插入一次都会出现新结果 |
PUT | UPDATE | UPDATE | 非安全 | 幂等 | 写操作非安全,一次和多次更新结果一致 |
DELETE | DELETE | DELETE | 非安全 | 幂等 | 写操作非安全,一次和多次删除结果一致 |
幂等性: 对同一REST接口的多次访问,得到的资源状态是相同的。
安全性: 对该REST接口访问,不会使服务器端资源的状态发生改变。
4.6 如何实现幂等性
幂等性是开发中一个至关重要且普遍存在的需求,特别是在支付、订单等与金钱直接相关的服务中,确保接口幂等性更是不可或缺。在实际开发过程中,我们应针对各种业务场景灵活选择幂等性的实现策略:
- 对于下单等具有唯一主键的业务场景,采用“唯一主键方案”是合适的选择。
- 当涉及更新订单状态等更新操作时,“乐观锁方案”则显得更为简便。
- 在上下游服务交互中,上游服务采用“下游传递唯一序列号方案”能更有效地确保幂等性。
- 面对前端重复提交、重复下单或缺乏唯一ID的场景,通过Token与Redis结合的“防重Token方案”能够迅速解决问题。
需要强调的是,实现幂等性必须深入理解自身业务需求,紧密结合业务逻辑进行实现,这样才能确保合理性。同时,要处理好每一个细节,完善整体业务流程设计,从而更有效地保障系统的稳定运行。
综上所述,幂等性的实现需以业务需求为导向,结合具体场景选择最佳策略,并注重细节处理,以确保系统的高效与稳定。
方案名称 | 适用方法 | 实现复杂度 | 方案缺点 |
数据库唯一主键 | 插入操作删除操作 | 简单 | - 只能用于插入操作;- 只能用于存在唯一主键场景; |
数据库乐观锁 | 更新操作 | 简单 | - 只能用于更新操作;- 表中需要额外添加字段; |
请求序列号 | 插入操作更新操作删除操作 | 简单 | - 需要保证下游生成唯一序列号;- 需要 Redis 第三方存储已经请求的序列号; |
防重Token令牌 | 插入操作更新操作删除操作 | 适中 | - 需要 Redis 第三方存储生成的 Token 串; |
1. 数据库唯一主键
利用数据库中的主键唯一约束,我们可以确保在数据插入时实现幂等性。关键在于,我们应避免使用数据库自增主键,而是选择分布式ID生成器(例如UUID、雪花算法等)来生成全局唯一的ID。这样,在分布式环境中,我们也能保证ID的唯一性。在实际操作中,我们无需在插入前查询数据库以检查记录是否已存在,而是直接尝试插入,并捕获可能的主键冲突异常。如果插入成功,则表示记录是新的;如果捕获到主键冲突异常,则表示记录已存在。
2. 数据库乐观锁
数据库乐观锁特别适用于更新操作中的幂等性保障。我们可以在数据表中添加一个版本标识字段,每次更新数据时,都会将这个版本标识作为条件之一。只有持有正确版本标识的请求才能成功更新数据。这种方法基于一个假设:即并发冲突是罕见的。因此,在更新之前,我们不会锁定数据。然而,一旦更新失败(通常是由于版本标识不匹配),我们就需要告知用户数据已被其他事务修改,并可能需要重新尝试操作。
3. 防重Token令牌机制
为了应对客户端连续点击或调用方超时重试等场景,我们可以采用Token机制来防止重复提交。在请求接口之前,调用方需要先向后端请求一个全局唯一的Token。在后续的请求中,调用方需要将这个Token作为请求头的一部分传递给后端。后端在接收到请求后,会在Redis中检查这个Token是否存在以及是否与用户信息匹配。如果检查通过,就执行删除Token的操作,并继续处理后续的业务逻辑。如果Token不存在或用户信息不匹配,就返回重复提交的错误信息。
4. 下游传递唯一序列号
在下游向上游请求服务时,可以附带一个唯一序列号来确保幂等性。上游在接收到请求后,会利用这个序列号在Redis中进行检查。具体做法是:将序列号和下游认证ID进行组合,形成Redis的键;然后检查Redis中是否存在这个键的键值对。如果存在,就表示已经对下游的该序列号请求进行了业务处理,此时可以直接返回重复请求的错误信息。如果不存在,就在Redis中存储这个键值对(键为序列号和认证ID的组合,值为下游传递的关键信息),并继续处理后续的业务逻辑。