【转】APP后端开发杂谈

Header头

header 头推荐加上字段:

  • Authorization

用来存放access_token,通过该token对用户进行认证。可以理解其作用等同于cookie中的session_id,有该令牌就默认用户已经认证登陆过。

  • Signture 客户端签名

用来判断该请求是否是客户端APP发起的请求,现在最常见的两种方法 一种 是通过ASE加密解密来实现。一种是通过比对信息摘要的方式。 最简单的生成方式是给出一个随机值和时间戳,还有和服务端约定好的盐值按照一定的顺序进行MD5加密。

signture = md5(nonce + timastamp + salt)

服务端收到该签名的时候,按照约定的顺序进行MD5加密比对签名,如果服务端生成的签名与客户端签名不一致,则可认为不是客户端发起的请求,此时不响应该请求。当然使用这种方法要同时提交timestamp 和 随机字符串nonce。

  • version 客户端版本

用来存放客户端版本号,主要是小版本,大版本的话一般服务端会开新接口。但是比较好的做法,是客户端每一次发布都要有一个版本号。就算是此次发布,服务端没做任何更改,只要客户端新发布也应该给一个新的版本号。然后将这个版本号写入header 头中。这样的好处是如果请求出现错误,我们能够定位到是哪个版本的APP出现了该错误,可以更容易的定位复现错误。更详细地还可以把设备的操作系统和型号也在header头提交。

  • timestamp 请求时间戳

客户端发送请求时的时间戳,可用于请求过期判断。


客户端签名细节

  • 防止重放攻击

虽然signture的存在,使我们可以判断该请求是否是客户端生成,从而只响应客户端的请求。但是这样安全性还是不够。

一个典型的攻击手法时,通过抓包客户端生成的签名不断地进行接口请求。最常见的就是利用该签名不停地请求短信验证码接口,直到服务器的验证码余额耗光。

防止重放攻击的最有效方式就是保持signture的唯一性。客户端必须生成唯一的signture,同时服务端也要保证对于一个signture只响应一次请求。

  • 保证客户端签名的唯一性

客户端保证每次生成一个唯一地signture的最简单方式是在生成签名的方法中加入时间戳。

服务端保证对每一个signture签名,只使用一次的实现方法是:对每次请求做判断,如果该请求携带的签名没有使用过,则响应该请求,并把该签名记录在服务端并标记为已使用。如果查到该请求携带的签名已经被标记为已使用,则不响应该请求。

服务端对signture做记录,一般通过3种方式:

  1. 写进文件
  2. 写进MySql数据库
  3. 写进Redis

写进文件的弊端,在于无法供分布式系统使用。MySql的弊端在于请求数多时增加了数据库的压力。所以最好的方式还是写进Redis里。

  • 增加签名超时机制

我们保证了客户端签名唯一性的方法是将每次请求的signtue记录在服务端。但是随着请求的增加,记录的signture也越来越多。每次请求都要逐一比对以前所有请求记录下的signture,这样显然是不合理。

所以我们正确的方式,不应该是单纯把signture写进文件,mysql,或redis。而应该是做一个文件缓存,或是写数据库临时表,或是redis缓存。

但是如果我们只比对缓存期内的signture,攻击者还是可以通过使用已到期被缓存清除的signture来进行重放攻击。于是我们引入了一个超时机制,如果该请求携带的timestamp 比当前的服务端时间相隔已经 大于singture缓存时间,则不响应这个请求。这样就保证了,攻击者无法使用被缓存清除的signture进行重放攻击。

当然超时时间也不能设置太小,因为客户端请求到达服务端需要一定的时间。所以超时时间的设置应满足

(服务端当前时间 - 客户端提交的timestamp) < 超时时间 < signture缓存到期时间

引入超时机制的另一个好处是防止了一部分的中间人攻击。因为劫持增加了请求的时间,因为超时机制的存在,可能使被劫持的请求失效。

  • 防止超时时间下溢攻击

引入了超时机制以后,可能我们一般都会这样写

if( (服务端当前时间 - 客户端提交的timestamp) < 超时时间 ){
    超时了;
}

但是如果攻击者 更改了客户端时间,使客户端提交的timestamp是一个比服务端时间还超前几天或是几年的时间戳,并生成了一个对应的signture。这个时候,当该请求响应后。攻击者等待一段时间,等缓存中这个signture 失效了,攻击者就可以拿着这个signture和timestamp 进行重放攻击了,因为这个timestamp 超前了服务端时间几天或几年,所以

服务端 - 客户端提交的timestamp = 负数 < 超时时间

所以,通过负数小于超时时间,绕过了超时机制,使signture又可以重新使用。当然这种情况下的重放攻击已经很弱了,因为signture使用过一次就会被缓存,所以通过下溢重新使用signture也要等到上一次signture的缓存失效了。则两次攻击之间,便必须隔一段缓存有效期。

所以,更合理的话,除了比对请求时间是否小于超时时间。还应该判断:

服务端时间 - 客户端提交的timestamp > 0

  • 保证客户端服务端的时间一致性

引入超时机制的前提是客户端和服务端的时间误差在可接受的范围。

试想一下,如果客户端请求需要0.5s到达服务端,所以服务端的超时时间设置了1s。但是客户端的时间比服务端慢了2s 。这个时候当客户端的timestamp 提交到服务端时 本来应该是

服务端当前时间-客户端提交的timestamp = 0.5 < 1 // 不超时

结果因为客户端时间比服务端时间慢了2s,使timestamp 到达服务端时,变成了

服务端当前时间-客户端提交的timestamp = 2.5s > 1 // 超时

所以客户端服务端时间不一致的会造成客户端所有请求都因为超时而无法响应。

那么如何保证服务端和客户端的时间一致性呢? 一个最常用的解决方式就是:

服务端给出一个接口返回当前时间戳,
客户端请求该接口获取时间戳,加上该请求的响应时间与当前时间戳相减得出时间差。
而客户端提交的timestamp 就是当前时间戳加上服务端与客户端的时间差。


客户端签名生成方式

加密 还是 信息摘要 ?

  • 信息摘要

信息摘要的好处在于服务端处理更简单,只需要生成对应的签名进行比对即可。

  • 加密

加密的方式生成签名常见的可以采用ASE加密,借鉴微信支付宝sdk签名的生成方式,把header头的重要的参数,都参与signture的生成。这样的好处在于更加安全,如果传输中header头的参数被劫持更改,会造成服务端验签失败,则请求自然就不响应了。

Timestamp

timestamp 是要提交10位还是13位的时间戳呢?

这个问题最常见于用PHP写的APP后端。因为PHP中使用的时间戳是10位的,time()也是只返回一个10位的时间戳。

但是,我还是认为应该使用一个13位的时间戳。至少你生成signture的时候应使用13位的时间戳,因为这样实时性更强,防止客户端太多的时候,不同的客户端同时生成签名时,出现signture相同的情况。

所以,反正客户端使用的是13位的时间戳,如果提交一个10位的时间戳,它也要进行截取。不如直接提交过来怎么使用,服务端自己决定的。

请求错误日志

APP开发的其中一个难点就是错误定位难,复现难。所以写日志就非常重要了。 当前错误日志所需记录的信息应包含至少以下几类信息:

  1. 发生错误的接口地址和时间
  2. 该请求的header头中的access_token,其实更合理地应该通过access_token获取用户id并记录,因为access_token是有可能更改的。
  3. 该请求的客户端版本号,更详细的话,还有操作系统和设备型号。这就是为什么前面提倡将这几个参数写在header头里每次请求都提交的原因。客户端版本号可以用来判断哪个版本请求会出现错误,然后再决定如何更改。操作系统和设备型号主要用于给前端兼容性错误排查。
  4. 该请求产生错误信息。
  5. 该请求的http状态码。
  6. 业务层如果有错误状态码也需要记录

请求返回格式

APP开发现在比较流行的还是返回json格式而不是xml格式。

返回json格式的数据一般是这样的:

{
    "status" :200,
    "message":"ok",
    "data"   :{},
}
status 返回请求状态码,一般复用http状态码。
message 返回请求消息,如果有错误这里写错误信息。
data 是返回的数据

但是我还是比较喜欢以下这种返回方式

{
    "code":0
    "data":{}
}
// 为什么请求成功 要使用0作为code状态码呢,0的第一感觉不是false吗?
嗯,错误情况千千万,而成功只有一种情况。正数负数千千万,而0也只有一个。
{
    "code":1001
    "message":"某个控制器请求出错"
}

为什么呢?

因为不想用http status来传达API请求状态,http status 传达的是通讯层的状态。API是为了满足业务,返回的数据应包含业务层的状态码。业务层不和通讯层耦合,不拿http status 取巧。

当然对于这点,喜欢使用http status 的同学也有不同的看法,这就看个人的喜好了。

我觉得使用code的好处在于:

  1. 我们可以自定义更多的状态码和错误信息。一般我会做一个接口错误地图类,然后根据code的值获取对应的message。
  2. 更好地对code进行分类定义,比如1000 开头的表示 a 控制器各个接口的产生的各种错误 2000 开头的表示 b控制器各个接口产生的各种错误。 -1 表示 错误地图类中 未定义的错误。
  3. 业务层的状态码不和通讯层状态码耦合,更详细地展示业务层错误信息。
  4. 避免客户端出现某个接口返回未考虑进去的非200 ok的http 状态码,而造成客户端卡死的情况。我喜欢在后端对http响应的状态码进行判断,如果该请求的响应码不是200 就把查看错误地图类转化为对应的code状态码和错误message,写入日志,并把http 状态码改回200。这样保证每次http请求基本都会返回200,可预知的错误都转化为返回的json数据中的code状态码。

Authorization

  • App后端开发不能使用session?

虽然app通过接口请求的方式与后端交互,没有cookie,但是依然可以使用session。session的实现不依赖于cookie,如果你把cookie中的session_id 但是打开session的令牌。那么header头中的Authorization 字段提交的access_token 同样可以看成令牌实现同样的作用。

  • 是否允许账户同时在两个以上的设备登陆

因为我们通过Authorization来获取认证,所以:

  1. 如果你允许同时登陆多台设备,你只需要登陆后复用user表中的access_token。

  2. 如果那你不允许同时登陆多台设备,则可以选择登陆时刷新access_token,这样就使得其他在线的设备请求头中的Authorization字段提交的access_token与user表中的不匹配,自然就被挤下线了。

  • access_token的安全性问题

我们通过access_token来获取用户,也就意味着access_token如果被劫持就等同于用户的账户被盗。

你想想同样作为获取服务端session的令牌,使用cookie时,为了安全我们一般会做哪些呢?

  1. cookie在生成时就会被指定一个Expire值,这就是cookie的生存周期,在这个周期内cookie有效,超出周期cookie就会被清除
  2. 对cookie进行加密,嵌入时间戳保证每次加密后的密文不同
  3. 不允许跨域使用

所以,虽然signture的唯一性已经为我们证明了是APP发起的合法请求,但是严格来说我们也不能单单对access_token 进行明文传输。 我们可以考虑在Authorization 字段不是简单地传输access_token的值,可以传一个access_token和时间戳的加密字符串,在服务端再进行解密,并先判断是否超时。如果要安全性高些,还可以参考signture做唯一性处理。


版本升级

建议建一个版本升级表用来存放版本升级信息。并且要有是否强制更新字段。

我们header头提交version参数,写日志为的都是不想失去对客户端的控制,能更好的定位错误。但是app与传统的web开发的一个区别,就是web开发页面做了修改,所有的用户都能看到修改,但是APP的话,只要用户没有更新,已修复的bug,对用户而言 依旧存在。

版本升级表设计

字段名 类型 备注
id int 主键id
app_type varchar 客户端版本类型 ios or android
version int 开发版本号
version_code varchar 客户端版本号(1.0.2)
upagrade_desc varchar 更新提示语
apk_url varchar 更新包链接
is_force tinyint 是否强制更新
created_at int 创建时间
status tinyint 是否已发布

有了版本升级表以后,我们就能更方便直观地管理查看我们发布的版本。

而且我们可以在打开APP时请求接口,查询版本设计表获得最新的版本与header头提交的version字段作对比,判断是否需要更新,弹出更新窗口。

对于需要强制更新的版本,弹窗应设置为不允许用户点击取消,一定要更新才能使用该APP。这样我们就可以把一些重大更新或者修复一些重要bug的版本设为强制更新,不更新就不让继续使用。

用户分析

为了更好地进行用户分析,我们还可以建一个APP登陆记录表。 打开APP时就通过header把用户信息记录起来,用来做用户分析。用户日活量,月活量。

客户端一打开就将数据发给该接口就行,不管请求是否成功,客户端都不需要关心。

app_active_log 表

字段名 类型 备注
id int 主键id
app_type varchar 客户端版本类型 ios or android
version int 开发版本号
version_code varchar 客户端版本号(1.0.2)
model varchar 设备型号 小米 苹果
uid int 用户id
created_at int 创建时间

这个表的另一个功能还可以统计某个版本的用户量或是Android还是IOS用户多,方便我们更新版本时选择先开发IOS版或是安卓版,或者出现bug决定哪个版本先修复。

客户端异常监控,分析

常见的APP端异常:

  1. crash 使用APP过程中突然出现闪退
  2. 卡顿 出现画面卡顿
  3. Exception 程序被catch起来的Exception
  4. ANR 出现提示无响应弹框(Android)

我们在服务端写日志,在header头提交设备信息这些都是为了更好地定位客户端的错误。但是我们的日志只能记录接口调用异常。对于客户端的异常却无能为力。

为此,我们应该和客户端配合。把客户端产生异常定期上报到服务端。方便客户端工程师定位复现并修复客户端异常

我们可以建一个 app_crap 表来统计收集 crash 卡顿 Exception ANR的次数和影响用户量 用户数

字段名 类型 备注
id int 主键id
app_type varchar 客户端版本类型 ios or android
version int 开发版本号
version_code varchar 客户端版本号(1.0.2)
model varchar 设备型号 小米 苹果
type tinyint 端异常类型 卡顿 闪退
description varchar 描述
created_at int 创建时间

当然,客户端记录这些数据比较麻烦.一个更好的解决方案是在客户端中集成第三方服务提供的SDK,将这些数据提交到第三方平台,客户端工程师可以登录第三方平台查看客户端异常统计。

常用的第三方平台 :

  1. 听云
  2. OneAPM

消息推送

  • 原生方式
  1. 客户端轮询 不推荐
  2. 服务端主动推客户端 实现难度大
  • 第三方推送服务
  1. 极光推送 推荐使用restful api接口 比其他SDK用起来更方便
  2. 百度云推送
  3. 信鸽

APP后端开发工具推荐

  • postman

接口调试神器

  • ngrok

内网映射工具。app开发一个麻烦的地方在于无法本地调试,因为客户端需要请求有域名或公网ip的服务端代码,虽然公司有测试服务器,但是有些时候测试服上有很多人同时使用,我git提交了修改后的服务端代码不能马上reset hard生效。或者测试服不在我开发的分支。ngrok的好处是内网映射,给你的电脑绑定一个域名。而客户端测试时填写这个域名能访问到你电脑的服务端代码,实时调试更方便。

  • guzzle

php一个http 请求包,通过composer 安装快速使用,可用来写接口的测试代码,模拟发起http请求,比起postman的优点在于,通过代码实现,自定义更方便。


原文:https://juejin.im/post/5a8eaee96fb9a0634417f4bf

你可能感兴趣的:(【转】APP后端开发杂谈)