前言
随着业务的快速发展,需求的快速变更,人员的增加和流动性,项目开发周期不断缩短,面对各种业务的新增和需求的变更,如果前后端还是使用老的古板的对接方式,各端会很难快速应对,更难以提高效率。所以前后端需要制定对接规范以便快速上线项目,使公司的项目可以安全,高效,可持续的交付,为平台化做准备。
经历路程
作为一个前端开发,一个好的app,除了需要流畅的交互设计,一个性能优异,结构清晰,维护便捷,安全,和高扩展性的接口设计也很重要,app的接口设计关系到app维护的混乱程度,以前公司每个迭代API字段的定义会邀上前端开发同学进行接口评审,从最初的excle表格定义接口文档,到markdown文档,再到小幺鸡,后面用Swagger,逐渐演变的文档的规范化,促进了开发效率的提升,沟通成本降低。API设计 没有最优的方案,都是需要在不断迭代中,找到越来越适合的方案。本文结合自己的浅薄见识,说一些app接口设计的一些规范,与大家分享共勉。
设计思想
APP与服务器的接口设计需要考虑很多地方,同时对服务端要求也是比较严格,在移动端有限的带宽条件下,要求接口响应速度更快,所以在开发过程中尽量选择效率高的框架,对数据要求也比较严格,APP需要什么数据就传什么数据,按需提供,不可多传,过多的数据量影响处理速度,也影响了传输效率。接口的数据的字段格式也要规范,以面向对象的思想设计接口,后面会讲到,这里可以跟前后端做接口评审,提高接口的规范程度。
先了解一下RESTFUL设计思想
RESTful架构是对MVC架构改进后所形成的一种架构,通过使用事先定义好的接口与不同的服务联系起来。
相应的会有一些风格的规定:
- 统一接口,可以用一个URI(统一资源定位符)指向资源。
- 无状态,即所有的资源,都可以通过URI定位,而且这个定位与其他资源无关,也不会因为其他资源的变化而改变.
现在很多厂应该都在使用REST框架,在项目API设计的时候,后端可能需要考虑的一些问题:
-
如何组织URL?
-
如何统一输出REST?
-
如何统一处理错误?
-
如何定义错误码?
REST的简单的应用就是面向对象设计,API的面向对象设计并不是面向页面设计,这样API就具有了多端不同展示的能力。在API设计中,要根据自己的业务类型和面向群体来综合考虑。app接口开发必须基于场景提供接口,按需提供接口,或者字段。
API文档化
文档化(xx项目开发文件夹)对一个项目来说很重要,这里可以整理每一个迭代的prd,通用配置文件,回顾会文档,开发规范,埋点文档,扩展文档,交互文档,方便因人员的流动性,新进来的人员可以快速通过文档熟悉了解项目整体规范。 再来说API文档,相信无论是前端开发还是后端开发,都或多或少地被接口文档折磨过。前端经常抱怨后端给的接口文档与实际情况不一致。后端又觉得编写及维护接口文档会耗费不少精力,经常来不及更新。其实无论是前端调用后端,还是后端调用后端,都期望有一个好的接口文档。但是这个接口文档对于程序员来说,就跟注释一样。所以仅仅只通过强制来规范大家是不够的,随着时间推移,版本迭代,人员的流动性,接口文档往往很容易就跟不上代码了。所以一个好的API文档管理很重要,这里推荐两个,一个是阿里妈妈前端团队出品的开源接口管理工具RAP第二代,一个是Swagger.
安全机制的设计(接口安全问题探讨)
在app跟服务端通信的过程中,一个很重要的考虑因素是通讯安全问题。没有绝对的安全,但要做好绝对的保障。常要求的接口安全如下:
-
防伪装攻击(如:在公共网络环境中,第三方 有意或恶意 的调用我们的接口)
-
防篡改攻击(如:在公共网络环境中,请求头/查询字符串/内容 在传输过程被修改)
-
防重放攻击(如:在公共网络环境中,请求被截获,稍后被重放或多次重放)
-
防数据信息泄漏(如:截获用户登录请求,截获到账号、密码等)
针对上面的要求,我们在设计的时候可以考虑下面的一些常用的安全措施:
- 使用https协议。客户端使用ssl pining(只比对伺服器凭证的 public key 跟你的凭证的 public key 是否匹配,防止证书过期app也能使用)增加抓包难度。
-
app跟后台不保存任何用户的密码明文。使用非对称加密,客户端通过公钥加密敏感信息,传输(如登陆账号,密码等等)。在正式的使用场景中一般都是对称加密和非对称加密结合使用,使用非对称加密完成秘钥的传递,然后使用对称秘钥进行数据加密和解密。二者结合既保证了安全性,又提高了数据传输效率。
- 客户端使用token机制,每次请求服务器发送一个事先在服务器端生成的token来做认证请求,token的生成规则服务端可以根据heard里面的值(用户的唯一标识)+ 时间戳 + MD5加密等等…,生成规则。token的时效性服务端控制。服务器端需要具备一套完整的Token创建和管理机制。一般流程如下:
-
- 用户用密码登陆成功后,服务器返回token给客户端;
-
- 客户端讲token保存在本地,发起后续的相关请求时,将token发回给服务器;
-
- 服务器检查token的有效性,有效则返回数据,若无效,分两种情况:token错误,这时需要用户重新登陆获取正确的token。token 过期,这时候需要跟后端约定什么样的接口code返回,判断跳转到登入页面,在发起一次认证请求,获取新的token。
-
- 或者可选客户端使用Cookie机制。
- 接口参数签名
-
- 接口需要添加的必要参数如appid(调用方身份ID),sign(参数签名值),timestamp(时间戳)。
-
- 可以按一定的规则对请求参数按key或者value做升序降序排列,然后拼接成字符串。
-
- 对第二部的字符串在拼接一个特有的值,在进行MD5加密得到摘要,在将摘要转化成大写。
-
- 上面的这些规则,你可以随意定制,跟后端统一规范,满足轻量级,易开发,易测试,满足接口安全的要求。
-
- 请求body加上md5签名验证(md5签名后可以做一个规则,截取md5前多少个字符,或者使用RSA在加密一次)。防止串改body再请求
- 防止重复请求攻击:
-
- 时间戳以在app每次启动和注册登录时和服务器同步时间。
-
- sign重复效验。 在请求服务端的时候会有一个sign(md5)签名。服务端可以把这个sign存储到redis中(可以设置5小时过期,2小时过期,具体看API每天的请求量),然后下次请求过来的时候,把redis中存储的和request相比较一下。如果重复,就认为是重复请求。返回报错。如果sign数据量暴大,可以采用分表分库存储redis的方式。当然缓存不一定是redis,其他的都行。要做一个缓存的时间,或者先进先出存储,希望多长时间不允许再次请求,就缓存过期时间设置多久。再智能一点,就是所有用户一开始默认30分钟。 当检测到用户有重复请求的时候,就给他单独设置8小时,黑名单机制。
-
关于客户端通用信息传递
通用字段的传递,一般服务端需要用户的很多信息去生成token,或者生成签名串, 常用的通用字段:
- 请求时间(时间戳)
- 设备ID
- 设备IMEI
- 生成的设备唯一标识
- 设备操作系统
- 网络类型
- 用户定位城市ID
- 请求token。
- 版本号
- 渠道
关于通用字段的传递,一般通过header传输(可选) 这里也可以只设计全部请求都是Post请求。 传的信息全部放在请求body里面。还有很多是通过url参数Encode传输。
服务端响应(Body)主体设计:
现在基本上的客户端跟服务端的数据交互都差不多采用是json的数据格式,所以这里讲解的都是以JSON为参考。
一. JSON的值的数据类型:
json的数据类型有六种:
- Number: 整数或浮点型
- String: 字符串
- Boolean: true 或 false
- Array: 数组包含在方括号[]中
- Object: 对象包含在大括号{}中
- NULL: 空类型
所以传输的数据类型不能超过这六种数据类型,这里列几种错误,比如传输Date类型,这种在转换的时候会产生问题,不同的解析库解析方式可能不同,有的可能会乱码,有的直接异常了。字符串出现“true”和“false”或者字符串数字,”null”等等,还有返回数组的时候,数组里面某个对象是null,这种真的很可怕,导致app崩溃。
二. 对于服务端返回报文的设计这个问题涉及到前后端的思考:
-
对于服务端来说,当REST API请求,如何封装返回报文?返回标准的报文格式? 出错时,如何返回错误信息?
-
对客户端来说,当客户端收到REST响应后,如何去解析报文,如何判断是成功还是错误? 这两部分必须得统一考虑。错误处理机制的设计的合不合理,是直接导致客户端如何处理API的噩梦。
-
客户端在处理REST API的错误时,会遇到哪些的REST API错误?
三. 这里作为前端,大致讲一下会遇到的两种类型的REST API错误:
-
一类是类似403,404,500等错误,这些错误实际上是HTTP请求可能发生的错误。REST请求只是一种请求类型和响应类型均为JSON的HTTP请求,因此,这些错误在REST请求中也会发生。这种错误,客户端可以识别,并且服务端也无法去操作HTTP服务器的错误码,对于这种类型的错误,客户端除了提示用户“出现了网络错误,稍后重试”以外,并无法获得具体的错误信息。常见的HTTP状态码可以看这遍文章HTTP状态码汇总
-
另一类错误是业务逻辑的错误,例如,输入了不合法的Email地址,密码输入错误,token过期,特殊错误码做不同的业务逻辑处理等等。这种类型的错误完全可以通过JSON返回给客户端,这样,客户端可以根据错误信息提示用户“Email不合法”,“输入的密码有误”,等,以便用户修复后重新请求API。
三. 服务端返回的标准报文格式:
每个公司对标准的报文格式定义不太一样,这里讲两个案例:
{
“success”: false,
"code": "10000",
"message": "错误的邮箱地址",
"data":<T>范型
}
-
这个格式的定义是,code跟success是对应的关系,只有在code 为1的情况下,success才为true,其余success都为false,code 错误码的不同值去处理业务的不同逻辑。 这样的方式其实对于前端来说非常不友好,一是我们是通过success还是code来判断这个请求已经成功了还是失败了(服务端代码错误),如code返回8006弹出一个提示窗,这时候success返回的是false,其实这个网络请求是成功的,因为业务的逻辑问题要返回一个业务逻辑的错误码给前端进行处理,但是却把success返回false,认为请求失败了,这样的模拟两可方式在我看来很不友好。 二是我们在做全局网络请求的interrupt的容易产生困扰。
-
针对上面的问题,对于服务端返回的通用的报文体大致可以定义成如下:
{
"code": "10000",
"message": "错误的邮箱地址",
"data":<T>范型
}
每一个API请求的返回里面都会有code字段 ,message字段(描述信息,成功时为“success”,错误时则是错误信息),data字段(成功时返回的数据,类型为对象或者数组)。code 为 0,表示请求成功,非0表示各种不同的错误。 不同的错误需要定义不同的返回码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误,这里举几个例子:
-
0: 成功;
-
100: 请求错误;
-
101: 缺少key;
-
102: 缺少签名;
-
103: 缺少参数;
-
200: 服务器出错;
-
201: 服务不可用;
-
202: 服务器正在重启
同时可以定义一些通用的code的业务处理逻辑,比如登录过期返回code:1005,客户端做拦截器,统一处理,弹提示,跳转到登录页。再比如返回code:1006,客户端做toast提示等等。
四. 前后端开发规范上对body也要做一些禁止:
禁止服务端因业务需求,在code,message的同级下面添加其他的业务字段。 如下面:
{
"code": "10000",
"message": "错误的邮箱地址",
"data":<T>范型,
"link_type":"https://www....",
"...":"",
}
五.关于接口分页的方式:
后端因人员的不同,每个人对接口分页的方式都按照自己的想法去实现,这造成了前端维护起来很困难,很容易出错,也会让新进入的成员产生困扰,所以请求分页的传递的方式需要统一。 比如,要么定义好page是第几页,pagesize是每页返回多少条数据,或者使用offset,limit的方式,不要每个人员写一套,有的在header里面传page,pagesize,并且有的用Page,page_size, 有的在请求body里面传,这样混乱的方式,不利于前端封装跟迭代理解。其次是关于分页返回的方式, 分页返回的字段跟定义也需要统一。
六. 关于时间参数字段的传递与接收
时间参数字段,前后端也可以以一致的格式字符串传递与接收。 如:‘2019-01-01 12:12:12’
七. 服务端跟客户端表情如何处理?
- 客户端发送带emoji表情的字符串,后端接收,该怎样处理?以怎样的格式保存进数据库里面?
- 后端查询出来后,以怎样的格式返回给客户端?
这里本人没有实际操作过,网上查了一下,大致的思路是客户端可以不做处理,服务端对emoji进行转码。因为Emoji表情是4个字节,而Mysql的utf8编码最多3个字节,所以数据插不进去,需要将Mysql的编码从utf8转换成utf8mb4,但是这个编码的前提是 MySQL版本大于 5.5.3,并非是支持varchar就行的。
API设计的扩展
- API版本管理:
- 应该将API的版本号放入URL。
- 采用多版本并存,增量发布的方式。
- 对于一个 API 或服务,应在生产中最多保留 3 个最详细的版本
- API降级: 服务端降级处理,可以做一个开关。比如像购物车底部会有猜你喜欢,推荐列表什么的。在访问请求压力比较大,或者促销的时候。将这些服务都关闭。
- API灰度支撑: API的灰度发布指的是不停止老版本,额外搞一套新版本,常常按照用户设置路由权重,例如90%的用户维持使用老版本,10%的用户尝鲜新版本。不同版本应用共存,经常与A/B测试一起使用,用于测试选择多种方案。
- APP本地数据缓存
规范总结:
- a. API开发需要确保接口帮助文档所有字段描述完整。
- b. API开发需要确保所有mock数据字段均有值,不能出现null,如果跟前端约定好的定义null,需要文档描述清楚,因为空字段在反序列化的过程中,会因为无法捕捉异常导致程序异常退出。
- c. 单次请求的数据量应限制到最低,列表数据30条以上应做成可分页。
- d. 前端不做数据业务数据运算以及合并。前端只进行数据展示。这么做的第一个原因是前端客户端对于数据运算的性能和标准不一致,容易造成端与端之间不统一,由服务端统一下发运算结果最好;另外一方面,在上线后,在出现显示规格变化的时候,由于客户端已经发放应用市场无法召回,可以通过服务端进行字段展示改变。
- e. 除分页场景以外,尽量减少请求的次数,减少接口的数量(这里减少接口的数量并不是增加耦合,而是很多开发人员喜欢过度分离,一个页面整好几个接口)。对于前端这种独立的系统,通信依靠电信运营商和设备,在网络不稳定的情况下,依赖多次请求的业务,在请求成功率上比较低。
- f. 建议请求区分head和body,将通用字段放置于head中(可选)。在很多需要验证权限的场景使用比较多。另外,head,body进行统一封装,对于接口开发字段统一标准有益,可以减少维护的碎片化。
- g. App与H5的接口不通用,如果非要使用同一接口逻辑,需要在服务端重新封装(服务端的底层逻辑是一样的,只是在应用层面需要做一层封装)。
- h. 接口评审并不一定需要开会统一评审,也可以是开发人员的私下交流。 无论是变更,还是设计,最后的接口定义方案,应由接口开发人员统一给出。前端人员组与组之间其实是不存在必然的信息同步的,不能依赖前端人员同步接口定义。另外,接口的变更的发起方是API研发人员,原则上应该由发起方通知相关人员。
- i. 存在多级结构的数据,对象字段不可以为空,应保留字段基础值。同不可以有null。
- j. 存在数组时,不应出现数组字段为空,若没有对应的数据,需给空数组。同不可以有null。
- k. 同一个字段只能有单一含义,不应对单一字段赋予多重含义。字段含义不直观会直接造成后期维护的成本,容易引发线上问题。
- l. 字段定义应先设计主数据字段。因为所有的业务都依赖于主数据字段。
上面都是想到什么就乱写点什么,希望给自己一个学而不思则罔思而不学则殆的警惕。
待执行并思考的地方
- 尽可能的缩小沟通的成本,包括接口对接,传参方式,测试,开发流程等。
- 花少量的时间写文档,保证90%的开发人员看懂所有的内容
- 哪怕不看文档,也能知道各种接口逻辑,调用的流程。
- 不重复写代码。
- 尽可能的写高可读性的代码。
- 为做平台化的系统做准备。