第2章 开发规范

2.1 API接口规范

2.1.1 API接口安全设计规范

App的数据来源就是API接口,所以API接口对App的重要性不言而喻。设计API接口首要考虑的就是安全机制。

本书将从以下3个方面来考虑怎么设计一个安全的API接口。

1. 防篡改

防篡改就是防止请求的URL参数值发送至服务器的时候被改动。

普通的API接口格式是xxx.html?key1=xx?key2=xx?key3=xx。我们采用sign签名的方式保证数据传输的正确性。

App一般会在公司的后台申请一个appKey和一个appSecret,这两个是一一对应的。appKey会作为一个参数写在URL中,然后再发送至服务器。appSecret则用于参与生成sign的计算。sign算法需要足够复杂,最好有一套自己的签名算法,而不是外界公开的签名算法。一般采用安全散列算法实现,如SHA1。

sign也要作为一个参数添加到URL中,和appKey一并发送至服务器,如xxx.html?appK ey=xx?sign=xx?key1=xx?key2=xx?key3=xx。服务器收到请求后,会通过appKey查找对应的appSecret,然后通过同样的散列算法,得到一个sign,最后比较一下两个sign是否相等。如果不相等则数据遭到篡改,废弃这条请求。

另外,关于appSecret有以下两种使用方式。

·appSecret直接写在客户端代码中,这样即可直接获取调用。

·appSecret还可以通过一个专门的接口getSign从后台获取。这种情况首先需要用户登

录,登录成功后,服务器返回一个accessToken参数,然后调用appSecret接口时需要带上这个accessToken参数。

2. 防重放

解决了数据被篡改的问题之后,还有一个问题就是,如果一条正常的请求数据被其他人获取到了,从而进行第二次甚至多次请求应该怎么办呢?

我们这里可以使用nonce + timestamp的解决方案。

1)nonce

nonce是一个随机数,由客户端生成,每次请求时将随机数作为一个参数发送给服务器。服务器会在数据库里查询是否有这个nonce,如果没有则是一条新的请求,进行正常处理即可;如果能查到已经存在这个nonce,则废弃这条请求。

nonce可以通过UUID.randomUUID().toString()来生成。

有个问题是,这个nonce在数据库中随着请求量的增大,产生的数据量也会越来越大。为了解决这个问题,我们可以采用timestamp时间戳的方式。

2)timestamp

时间戳是服务器给URL请求设定的一个有限时间范围起点。例如服务器认为客户端发送过来的timestamp与服务器当前的时间戳之差在10分钟之内,则认为这条请求是有效的。超过了10分钟则废弃这条请求。如果是10分钟内的请求,需要在数据库中查询nonce是否有记录,如果有记录,则废弃这条请求;如果没有记录,则记录这个nonce,并且将超过10分钟的nonce全部删除。

关于这个timestamp获取的问题,同样需要从服务器获取,不然客户端怎么知道服务器的起点计算时间呢?上面说到有个获取appSecret的接口,其实我们可以在这个接口中一并将timestamp获取到。

需要注意的是,客户端需要在每次发送URL请求的时候,计算一下timestamp的值。例如获取到的timestamp=1564588800,那么下次请求的timestamp的值是1564588800 + diffTime。服务器收到timestamp后会跟服务器当前的时间戳做对比,看是否大于10分钟。

一条正常的URL格式请求如下:


demo.html?nonce=xx?timestamp=xx?appKey=xx?sign=xx?key1=xx?key2=xx?key3=xx

所以一个正常的API请求应该是这样的流程:用户登录成功后,每次进行一个API请求,都需要调用一次getSign接口,用于获取appSecret和timestamp。但是肯定不是每次请求都要获取一次这个接口的数据。我们可以在首次请求后,将这些数据保存起来,后续API请求可以直接使用,除非appSecret或者timestamp为空(例如App退出登录后清空appSecret和timestamp)。

3. HTTPS

HTTPS是用SSL + HTTP构建的可用于网络传输以及身份认证的网络协议。HTTP使用明文通信,传输的内容可以通过抓包工具截取,HTTPS自动对数据进行加密压缩,防止监听,防止被抓包以看到明文,防止中间人截取。

苹果从iOS9就开始默认使用HTTPS,Android 9.0也开始强制使用HTTPS了,默认阻塞HTTP请求。如果需要在Android 9.0中兼容HTTP,则需要进行额外的特定配置。

2.1.2 API接口通用设计规范

1. 版本号

每一组API接口需要对应一个大版本号,大版本号一般是跟App的大版本对应的。例如App第一版本命名为v1,App第二版本经过改版后,接口返回的内容一般也会有变化,这里命名为v2。

以Restful API风格为例,如/api/v{x}/,一般在API接口的前面位置加上v{x}这个值。

x分以下两种情况。

·整型表示大版本号,如v1、v2。

·浮点型表示小版本号,是对大版本定义的业务接口的补充,如v1.1。

举个例子,如下。

/api/v1/userinfo:表示v1这个大版本的App,有一个userinfo业务类型的接口。

/api/v2/userinfo:表示v2这个大版本的App,有一个userinfo业务类型的接口。

/api/v2.1/userinfo:表示在v2这个大版本中,对userinfo这个业务接口进行了一些细微调整。

2. 请求参数

请求参数由公共请求参数和业务请求参数组合而成。

1)公共请求参数

公共请求参数如表2.1所示。

表2.1 公共请求参数

其中version表示客户端版本号,此处将其传给服务器,由服务器根据客户端版本号进行一定的业务逻辑判断。

2)业务请求参数

以登录为例,表2.2展示了登录API接口的业务请求参数。

表2.2 业务请求参数

一个完整的URL如下(以传统的URL格式为例):

/api/v1/getUserInfo.do?nonce=3ec934e8-81b9-492e-933d-a5dc41eb15bd&timesta mp=1567118072&sign=c1741210c06f3827d1d2f3bfd1f1fc2878377307&accessToken=Lv zFEeQSuU9B4DrnoPO9D4CL3ZhKCetZ%2FRckCWQlgb9qmNLmgCKxkymoC4pEs5LF w1lSAGZXKvHe%0AvpFgXKmAAQ%3D%3D&appKey=3b6af23db66c69b7131a8186f90f e663&name=jack&password=123456

3. 返回值

使用JSON格式(而不是XML)返回API请求的结果。JSON格式简洁,传输数据量小,而且能展示复杂的数据结构。


1. {
2. "body":{
3. },
4. "code":0,
5. "msg":""
6. }

·code:API接口执行状态,例如0表示成功,-100表示网络超时,-200表示鉴权失败等

·msg:非成功状态下需要说明的信息,一般与code状态码一一对应定义。

·body:返回的具体数据,通常是JSON格式。

客户端需要对所有code的值进行逐一处理。

4. 接口变更

一般来说,一个API接口投入使用后,除非这个接口确定废弃不再使用,不然一般情况下不能对这个接口进行修改。例如修改了API的请求参数和返回值,会对使用此API接口的App带来不可预估的影响,最严重的影响就是崩溃。

如果接口需要变更,那需要保证API接口的变更能够向下兼容,就是API的变更不影响原来使用API接口的客户端。如果不能够保证向下兼容,那么只能建立一个新的API接口。

接口变更有以下两种情况:能够向下兼容;不能向下兼容,需要建立一个新的API接口。以下是一个原始的JSON格式的返回值:


1. {
2. "id": 1,
3. "name": "j ack",
4. "address": "usa"
5. }

原始URL: /api/v1/userinfo

1)向下兼容

·URL的请求参数新增字段,例如:


/api/v1/userinfo?currentTime=2019/12/12

新增一个请求参数currentTime,不影响当前API接口的使用。

·URL的返回数据结构新增字段,例如:


1. {
2. "id": 1,
3. "name": "j ack",
4. "address": "usa"
5. "age": 18
6. }

新增一个age字段,旧版本的App只需要id、name、address 3个字段,多出来的age字段对旧版本来说没有用处,也不会被额外处理。而新版本的App使用到这个接口时,可以对这4个字段进行解析,将新增加的age字段利用起来。

2)建立一个新的API接口

如果返回的数据格式字段值类型发生了变化,例如age原先是数字类型,值为18,现在改为字符串类型,值为“18”,这样旧版本的App解析的时候可能就会出错,影响使用。


1. {
2. "id": 1,
3. "name": "j ack",
4. "address": "usa"
5. "age": "18"
6. }

或者直接将age这个字段的名称改变了,变成year;或者直接将age这个字段给删除了。这些情况对当前使用这个API接口的App一定会产生影响,所以如果出现这种情况需要增加一个新的接口,而不是在原有的接口上修改。

5. 传统格式API

传统格式API: /getUserInfo.do?id=10000&netType=wifi

API由具体的业务地址(getUserInfo.do)和请求参数(id=10000&netType=wifi)拼装而成。请求类型一般就get和post两种,而且有时候在实际项目中这两种类型的请求并没有区分得很详细,两者都可以使用。

6. Restful API

Restful API: /api/v1/userinfo

Restful API通过URI(如api/vi/userinfo)来表示资源,通过GET、POST、PUT、DELETE等方法来表示操作行为。

·GET:获取资源。

·POST:新建资源。

·PUT:更新资源。

·DELETE:删除资源。

URI一般使用名词命名,例如GET/userinfo,表示获取全部用户的信息;GET/userinfo/100,表示获取id为100的用户信息。