App架构设计经验谈:接口的设计

原创文章,转载请注明:转载自Keegan小钢
并标明原文链接:http://keeganlee.me/post/architecture/20160107
微信订阅号:keeganlee_me
写于2016-01-07


App架构设计经验谈:接口的设计
App架构设计经验谈:技术选型
App架构设计经验谈:数据层的设计
App架构设计经验谈:业务层的设计
App架构设计经验谈:展示层的设计


App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉。

安全机制的设计

现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程是:

  1. 用户用密码登录成功后,服务器返回token给客户端;
  2. 客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;
  3. 服务器检查token的有效性,有效则返回数据,若无效,分两种情况:
    • token错误,这时需要用户重新登录,获取正确的token
    • token过期,这时客户端需要再发起一次认证请求,获取新的token

然而,此种验证方式存在一个安全性问题:当登录接口被劫持时,黑客就获取到了用户密码和token,后续则可以对该用户做任何事情了。用户只有修改密码才能夺回控制权。

如何优化呢?第一种解决方案是采用HTTPS。HTTPS在HTTP的基础上添加了SSL安全协议,自动对数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,安全性可以提高很多。不过,SSL也不是绝对安全的,也存在被劫持的可能。另外,服务器对HTTPS的配置相对有点复杂,还需要到CA申请证书,而且一般还是收费的。而且,HTTPS效率也比较低。一般,只有安全要求比较高的系统才会采用HTTPS,比如银行。而大部分对安全要求没那么高的App还是采用HTTP的方式。

我们目前的做法是给每个接口都添加签名。给客户端分配一个密钥,每次请求接口时,将密钥和所有参数组合成源串,根据签名算法生成签名值,发送请求时将签名一起发送给服务器验证。类似的实现可参考OAuth1.0的签名算法。这样,黑客不知道密钥,不知道签名算法,就算拦截到登录接口,后续请求也无法成功操作。不过,因为签名算法比较麻烦,而且容易出错,只适合对内的接口。如果你们的接口属于开放的API,则不太适合这种签名认证的方式了,建议还是使用OAuth2.0的认证机制。

我们也给每个端分配一个appKey,比如Android、iOS、微信三端,每个端分别分配一个appKey和一个密钥。没有传appKey的请求将报错,传错了appKey的请求也将报错。这样,安全性方面又加多了一层防御,同时也方便对不同端做一些不同的处理策略。

另外,现在越来越多App取消了密码登录,而采用手机号+短信验证码的登录方式,我在当前的项目中也采用了这种登录方式。这种登录方式有几种好处:

  1. 不需要注册,不需要修改密码,也不需要因为忘记密码而重置密码的操作了;
  2. 用户不再需要记住密码了,也不怕密码泄露的问题了;
  3. 相对于密码登录其安全性明显提高了。

接口数据的设计

接口的数据一般都采用JSON格式进行传输,不过,需要注意的是,JSON的值只有六种数据类型:

  • Number:整数或浮点数
  • String:字符串
  • Boolean:true 或 false
  • Array:数组包含在方括号[]中
  • Object:对象包含在大括号{}中
  • Null:空类型

所以,传输的数据类型不能超过这六种数据类型。以前,我们曾经试过传输Date类型,它会转为类似于"2016年1月7日 09时17分42秒 GMT+08:00"这样的字符串,这在转换时会产生问题,不同的解析库解析方式可能不同,有的可能会转乱,有的可能直接异常了。要避免出错,必须做特殊处理,自己手动去做解析。为了根除这种问题,最好的解决方案是用毫秒数表示日期。

另外,以前的项目中还出现过字符串的"true"和"false",或者字符串的数字,甚至还出现过字符串的"null",导致解析错误,尤其是"null",导致App奔溃,后来查了好久才查出来是该问题导致的。这都是因为服务端对数据没处理好,导致有些数据转为了字符串。所以,在客户端,也不能完全信任服务端传回的数据都是对的,需要对所有异常情况都做相应处理。

服务器返回的数据结构,一般为:

{
    code:0,
    message: "success",
    data: { key1: value1, key2: value2, ... }
}
  • code: 返回码,0表示成功,非0表示各种不同的错误
  • message: 描述信息,成功时为"success",错误时则是错误信息
  • data: 成功时返回的数据,类型为对象或数组

不同错误需要定义不同的返回码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误。这里举几个例子:

  • 0:成功
  • 100:请求错误
  • 101:缺少appKey
  • 102:缺少签名
  • 103:缺少参数
  • 200:服务器出错
  • 201:服务不可用
  • 202:服务器正在重启

错误信息一般有两种用途:一是客户端开发人员调试时看具体是什么错误;二是作为App错误提示直接展示给用户看。主要还是作为App错误提示,直接展示给用户看的。所以,大部分都是简短的提示信息。

data字段只在请求成功时才会有数据返回的。数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。这里需要注意的就是,不要将data传入字符串或数字,即使请求需要的数据只有一个,比如token,那返回的data应该为:

// 正确
data: { token: 123456 }

// 错误
data: 123456

接口版本的设计

接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:

  • 数据的变化,比如增加了旧版本不支持的数据类型
  • 参数的变化,比如新增了参数
  • 接口的废弃,不再使用该接口了

为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:

  1. 每个接口有各自的版本,一般为接口添加个version的参数。
  2. 整个接口系统有统一的版本,一般在URL中添加版本号,比如http://api.domain.com/v2。

大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。

如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。

有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。

写在最后

关于接口设计,暂时想到的就这么多了。各位看官看完觉得有遗漏或有哪些需要优化的欢迎提出一起讨论。

PS:于2016-04-18
之前code我解释为状态码,可能导致有些人将其和request status code混为一谈,因此改为了返回码,其含义其实和以下地址中的微信公众平台的返回码是一样的。
http://mp.weixin.qq.com/wiki/10/6380dc743053a91c544ffd2b7c959166.html


扫描以下二维码即可关注订阅号。

  • 有态度网友06MY5V2016-01-07 17:22

    沙发

  • 有态度网友06MY6t2016-01-08 14:21

    good

  • 书剑零落2016-01-08 15:28

    收藏

  • 有态度网友06MY5V2016-01-08 17:01

    这么垃圾的文章,居然出现在csdn首页上

  • 有态度网友06MY5Z2016-01-08 18:12

    你觉得垃圾,那你有高级的分享吗?

  • 有态度网友06MY6z2016-01-08 20:48

    可能是你看不懂所以觉得垃圾吧[嘘]

  • change_ck2016-01-08 21:39

    顶。。。

  • Hello-_-强2016-01-09 15:40

    不错。。。good

  • 李连强2016-01-10 19:52

    参考

  • 有态度网友06MY5V2016-01-11 09:08

    大神,厉害,我看到觉得很棒啊。

  • 有态度网友06MY5V2016-01-11 09:13

    像你这种人· 早应该滚出CSDN的圈子

  • 有态度网友06MY6F2016-01-11 10:22

    写的很不错

  • 有态度网友06MY5V2016-01-11 13:43

    那你分享点好的出来呗!

  • 有态度网友06MY6I2016-01-16 15:45

    是拦截到你的登陆接口,即使账号密码加密了,他有你加密之后的账号密码,没必要知道你的密匙也可以重复访问你的接口吧。好像这是所谓的重播攻击?这样的情况怎么处理?真心请教。

  • 有态度网友06MY5Z2016-01-25 16:36

    你的意思是攻击者不断重复发送同一个接口吗?

  • 有态度网友06MY6M2016-01-26 15:53

    加上时间,比如一个请求在10秒内有效?

  • 有态度网友06MY5Z2016-01-26 21:49

    嗯嗯,一般在参数中加入时间戳来防止部分重发攻击

  • 有态度网友06MY6Q2016-02-14 10:59

    深圳的连强[汗]

  • 有态度网友06MY5V2016-02-23 17:40

    我觉得很好,我们现在有些设计有类似的理念

  • 种黄瓜的小菜2016-03-01 08:42

    “数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。”钢哥,这句话可以这样理解不。举个栗子:单个数据的话,data:{key:value}。多条数据的话data:{result:[{},{},{}]},还是说data可以直接是一个数组,也就是data:[{},{},{}]。希望钢哥能为我解惑!!!

  • 有态度网友06MY6i2016-03-01 09:52

    之前做过app用户这块,对接口定义以及设计有个概念,大体设计上都是按照钢哥这篇文章设计的。有个问题就是 Token 刷新的问题,如果Token错误的话肯定是要客户端再次登录,但是token过期需要再认证一次是什么意思?跟再登录一次有什么区别?我印象中是服务端自动刷新这个token时间,比如说token生命周期是2天,如果两天之内再没请求过就会要求重新登录,两天之内连续登录的就会不停地刷新这个时间,这样是不是就是服务端控制不需要客户端认证?

  • 有态度网友06MY5Z2016-03-01 10:43

    直接是一个数组

  • 有态度网友06MY5Z2016-03-01 10:50

    服务端自动刷新是方便了,但我觉得安全性相对就差了些,刷新token由客户端发起安全性就高一些。另外,客户端刷新token也不是说让用户自己再登录一次,而是客户端自动调用认证接口

  • 种黄瓜的小菜2016-03-01 12:49

    好滴,看钢哥的文章学到很多!

  • 有态度网友06MY5V2016-03-01 14:57

    作者,请问下,
    为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:
    每个接口有各自的版本,一般为接口添加个version的参数。
    如果参数发生了变化,老版本如何兼容新版本的api?

  • 有态度网友06MY5Z2016-03-01 15:09

    你的参数发生了变化,是新增了参数还是减少了参数?

  • 有态度网友06MY5V2016-03-02 15:43

    新增、减少都有可能的呀

  • 有态度网友06MY5Z2016-03-02 21:08

    新版本兼容旧版本就有,还想旧版本兼容新版本?

  • 有态度网友06MY772016-03-07 17:03

    感觉博主将 返回 json 字段里添加 code 字段进行错误提示不是太好,你这样返回是不是任意 API 请求 都会返回 200 OK。个人感觉 http status code 就可以表达这些信息。
    200:成功
    404:请求错误
    422:缺少appKey
    403:缺少签名
    422:缺少参数
    500:服务器出错
    503:服务不可用
    503:服务器正在重启

  • 有态度网友06MY772016-03-07 17:04

    个人浅见,请指教。

  • 有态度网友06MY5Z2016-03-08 11:13

    你这是http本身的status code,但json里的code更多是业务上的code,两者是不同的,所以也不建议用http status code来代替业务的code

  • 有态度网友06MY772016-03-08 14:21

    恩恩 博主说的有道理,可假如你文中所指 缺少签名返回 code102,那 http status code 返回多少 200吗?还是403?

  • 有态度网友06MY5Z2016-03-08 15:33

    http status code肯定是200啊,因为请求发送成功了

  • 有态度网友06MY7f2016-03-21 21:39

    手机号+短信验证码的登录方式这种方式不错,问下app端单点登录怎么做的?就是同一个账号不能在多台手机同时登录

  • 有态度网友06MY5Z2016-03-22 09:04

    单点登录最简单的方案就是在服务端保存用户登录的状态,以及已经登录的这台手机的UUID之类的唯一标示,其他手机发起的登录就可以拒绝了

  • 有态度网友06MY7l2016-03-24 10:56

    不错,谢谢分享

  • 有态度网友06MY5V2016-03-25 11:52

    status code尽量遵循HTTP协议的status code. 只有成功接收并处理返回结果才是200 其他的按照相应的http status code进行返回。这样处理逻辑比较清晰,封装到json里客户端要处理两种异常。

  • 有态度网友06MY5Z2016-03-25 14:31

    我不太认同,就如我在上面说的,http的status code和json的code功能不同,按照你说的方式虽然看起来处理逻辑简单了,但却破坏了单一职责原则,没有区分开,我觉得不是明智的做法。

  • 有态度网友06MY5V2016-03-25 15:39

    功能没有什么不同都是标识请求结果的状态。 客户端的一个请求如果请求缺少参数, 那么对于HTTP Server说来那就是一个bad request,RESTful的design是返回的结果要按照要有一定的描述性。比如POST一个新的resource RESTful的API 返回201 而不是200。你了解单一原则,但是好像用错了地方。[呵呵]

  • 有态度网友06MY5Z2016-03-25 15:45

    缺少参数是一个bad request,但json的code返回的并不是这样的错误信息,而是比如:密码错误,accessToken过期之类的业务型的错误信息,这跟404啊201之类的status code是不同的。所以我才不喜欢将其和status code一起混用。

  • 有态度网友06MY5V2016-03-25 16:46

    Client Error在HTTP Status Code里面一共有30几个。 像密码错误, token过期其实都可以抽象到某一个或者几个status code,并不是说400 bad request就没有其他信息了。Windows Azure的Error定义供参考 https://msdn.microsoft.com/en-us/library/azure/dd179357.aspx

  • 有态度网友06MY5Z2016-03-25 17:01

    谢谢你提供的链接,也谢谢你一再地解释。其实我明白你的想法,不过还是那句话,我觉得两者的职责范围是不同的,所以我还是倾向于分开。原谅我坚持我的想法,我是一个比较固执的人。^_^

  • 有态度网友06MY5V2016-03-25 18:27

    没有最好的设计,只有最适合的设计。[呵呵]

  • 挨踢实习生2016-03-26 14:39

    你好,上边提到的:「token过期,这时客户端需要再发起一次认证请求,获取新的token」,是通过什么方式重新获取token?

  • 有态度网友06MY5Z2016-03-26 14:50

    最简单的方式就是再发起一次登录请求,一般客户端都会缓存用户登录信息的

  • 挨踢实习生2016-03-26 14:58

    重新发起登录请求的话不是就要用到用户密码了吗,这时候是不是还需要用户输一遍密码?缓存密码肯定不能吧。

  • 有态度网友06MY5Z2016-03-26 15:25

    那你们的app用户每次启动会自动登录吗?还是每次都要用户自己再登录一次?

  • 有态度网友06MY7w2016-03-26 19:38

    可不可说, 你只是将http status code映射到了json code里面?

  • 有态度网友06MY5Z2016-03-26 19:49

    No,两者是分离的

  • 有态度网友06MY7x2016-03-26 19:52

    这位同学,把你的牛逼的文章,分享出来,上CSDN首页,让大伙学习下!

  • 有态度网友06MY7w2016-03-26 20:10

    那比如说, 你是怎么知道这次请求是token过期的? 难道不是http status code 返回401的时候, 你知道这次访问是token过期了, 然后你再将业务code写进json的吗?

  • 有态度网友06MY5Z2016-03-26 21:04

    token过期的错误是服务端返回的,是json的code和message返回的结果

  • 有态度网友06MY7w2016-03-28 09:13

    嗯, token过期是服务端返回的, json数据也是服务端返回给客户端的.
    我的意思是, 服务端根据token过期的错误码(401, http status code), 然后再填充json的数据(code: 101, message: token invalid), 那是不是, 其实错误状态码都是服务端先知道, 然后再返回来客户端, 只是服务端把错误状态返回给客户端之前细化了状态码(比如, 服务端状态码:404, 对应客户端可能就有多个错误状态, 102(缺少appkey), 103(缺少其他参数))

  • 有态度网友06MY7I2016-04-01 10:31

    已举报,不说反对的理由,就知道了喷[怒]

  • 反革命攻城狮CasaTa2016-04-11 20:39

    大家可以去参照casatwy.com的架构谈文章,比这里出的早,而且这里的思想其实都在casatwy.com找得到。

  • 有态度网友06MY802016-04-15 16:27

    我来说说反对理由:
    返回状态码都能搞错???
    2XX才是成功把?
    5XX是服务端的问题
    太差

  • 有态度网友06MY802016-04-15 16:29

    太逗,他request code 还加到json里面,前端要判断几次?

  • 有态度网友06MY5Z2016-04-15 16:57

    关于返回码,其实也可以参考微信平台的定义。
    http://mp.weixin.qq.com/wiki/10/6380dc743053a91c544ffd2b7c959166.html

  • 有态度网友06MY7r2016-04-15 19:33

    为什么这么多人不理解 code message data 里的 code 呢,非得跟 http 请求的状态码扯到一起...

  • CJY.2016-04-18 10:11

    +1 有学习
    从撕逼贴过来的

  • 有态度网友06MY5V2016-04-18 11:13

    后面不是有一段加密么。你可以把时间戳也放进去 一起加密。。。

  • 有态度网友06MY802016-04-18 11:36

    ** code: 状态码,0表示成功,非0表示各种不同的错误 **
    状态码or错误代码?
    错误代码我没得说
    状态码是搞笑么?

  • 有态度网友06MY802016-04-18 12:08

    code可以理解,但是写成状态码就不对了吧??怎么说也是放回错误代码吧

  • 有态度网友06MY5Z2016-04-18 14:27

    你说的是,是我的错,准确点说的确不应该叫状态码,这也许就是让各位觉得和request status code一样的原因了。我修正一下

  • 噁魔獵掱2016-04-21 21:13

    你好,大神。问题一:关于你上面说的将密钥跟几个参数生成签名?那是只把参数,签名发给服务器,然后服务器把密钥重新算出来吗?还是把密钥,参数,签名都发给服务器端?问题二:客户端发送的Token,过期和错误如何从服务器端区分?谢谢

  • 有态度网友06MY5Z2016-04-21 22:10

    问题一:密钥是用来生成签名的,本身不发送给服务端,发给服务端的是参数和签名;问题二:token在服务端是有记录的,就算过期了也是有记录的,但错误的token则没有记录。

  • 噁魔獵掱2016-04-22 00:41

    那问题一就是服务器端要根据参数和签名逆向把密钥算出来吧?(我还想问问那密钥就是上面提到token吧?)谢谢

  • 有态度网友06MY5Z2016-04-22 12:49

    密钥和token是不同的两个东西,密钥是用来加密用的,token是用来区分不同用户的。服务端也不是逆向把密钥算出来,密钥是服务端和客户端都分别有保存的,客户端和服务端用同一个密钥,同一种加密算法生成签名,两端计算出来的签名一致才表示这次通信是安全的。

  • 噁魔獵掱2016-04-22 13:18

    小弟新手,可能问得比较多,那楼主说的token可以理解为是sessionid吗?还是说我不一定要用session,只要把token保存到数据库,到时查数据库跟用户提交的token去比较?

  • 有态度网友06MY5Z2016-04-22 16:35

    token和session差不多

  • 小石头ys2016-04-26 16:54

    非常好

  • 有态度网友06MY5V2016-05-06 08:20

    首先从概念上来分析,http status code 更注重的是网络请求的状态返回,不论你的client error 定义的多全,由于从其本质上来说它无法等同或者取代于业务处理错误的返回码。
    其次,随着app的功能的逐渐增加,server端的扩展,各种不同的业务处理的意外会增加,等到那时候在考虑定义业务处理错误码就成本太高了。所谓回头太难就是这种情况。

  • 有态度网友06MY8w2016-05-09 17:42

    version这里增加个version的参数,接口里面是要根据version参数的值来区分调用不同版本的接口(是不是都是用if或者switch判断?有没有其他跟快速的),这里每次新增接口版本都是需要手动添加判断的是不,有没有什么办法,可以自动判断的(即每次新增version后,不用手动在去添加version的值)?

  • 有态度网友06MY5Z2016-05-11 14:21

    可以写一个dispatch方法之类的进行分发啊,不同版本分派给不同方法处理

  • 有态度网友06MY5V2016-05-18 14:36

    他这里说的是业务返回码

  • 有态度网友06MY802016-05-19 12:49

    你看我4.18的回复,谢谢

  • 有态度网友06MY8F2016-05-30 16:06

    万分感谢!很有指导意义

  • 有态度网友06MY5V2016-06-06 19:07

  • 有态度网友06MY5V2016-06-27 15:53

    应该重新设计一套自己的码表,避免和Http status code重复有歧义

  • 有态度网友06MY5V2016-06-29 12:57

    关于api签名部分 ,可否写个demo看看,文字描述总有种隔靴搔痒的感觉

  • isunimp2016-07-26 17:03

    不用和他解释,他连http状态码和这个返回码的区别都没有搞清楚

  • isunimp2016-07-26 17:09

    “给客户端分配一个密钥,每次请求接口时,将密钥和所有参数组合成源串,根据签名算法生成签名值,发送请求时将签名一起发送给服务器验证”
    这句话里面涉及到的参数应该怎么传呢,放在url还是body里面,放在url意味着会暴露这些参数,请求还是可以被伪造,放在body里面的话就必须使用post请求,可以解答下这个问题吗?

  • 有态度网友06MY5Z2016-07-27 09:37

    参数会不会暴露不在于使用get还是post,而在于有没有加密~而签名目的是为了认证,认证该请求的发送方是否可信,与加密无关。加密可以用AES或RSA。

  • isunimp2016-08-01 11:01

    谢谢指点,另外能否开一篇文章谈谈oauth2如何实现

  • 有态度网友06MY5Z2016-08-01 13:49

    oauth2网上的资料已经很多了,搜一下就有了

  • 有态度网友06MY5V2016-08-01 17:50

    哥,你把http code和业务的code混在一起用,我只能说,你太TM强大了!

  • G2016-08-03 17:03

    苹果不是要求17年就必须 全部 https了吗,还有半年

  • 有态度网友06MY9l2016-08-15 16:34

    学习~小肛..

  • 有态度网友06MY5V2016-08-23 05:25

    不错学习了,谢谢分享!

  • 有态度网友06MYa82016-11-14 12:46

    关于状态码部分:像服务器错误,客户端参数错误这种类型的错误是不是应该直接设置 response header 里的 status code,而具体业务上的错误使用返回数据里的 code

  • 有态度网友06MYao2017-02-25 00:28

    密钥放在客户端,怎么防止别人获取?wap的js,ios/android反编译

  • Postbird2017-04-02 23:13

    这篇文章已经发了一年多了,不知道作者还能否进行回复。
    目前需要做一个APP的API开发,之前从没接触过,看了你的文章,比较有头绪了。
    有几个问题想请教一下:
    1、基于token的验证类似jwt这种,只是在涉及用户状态的时候才有必要使用吗?(比如用户获取和修改自己的信息的时候,需要进行token的认证,http请求head中加入token。)
    2、基于token 的验证,比如jwt这种验证方式,发过来的实际上就是一串加密码,客户端也需要对加密码解出来?获得用户的信息?(如用户登录,请求一个认证uri,然后服务器返回了token和一些信息,客户端对于token 的操作应当只是作为一个验证还是说需要去解token,换句话说用户的信息应当在返回的json data中返回吗?)【可能这个问题问的有点傻X,毕竟第一次接触API的设计和开发】
    3、另一个就是appkey和appsecret的问题,存储在客户端的appsecret是否会被窃取?(比如android APP能否获取代码内容?【这个我以前做c#的时候就是各种怕被反编译而加壳..,java APP不是很了解】)
    可能上面问题有些小白,期待博主能解答 谢谢

  • 有态度网友06MY5V2017-04-26 09:32

    客户端自动调用认证接口是指什么?用什么请求参数?原有的过期的token?

  • Bob_yang9122017-05-05 10:23

    这个...要是个别用户的手机时间不准,和系统时间不统一,就会出现请求不到数据的可能(以前项目中踩过这个坑)

  • 丶晨小曦2017-06-13 18:01

    一般是以获取的北京时间或者淘宝时间为准