APP后端开发杂谈

Header头

header 头推荐加上字段:php

  • Authorization

用来存放access_token,经过该token对用户进行认证。能够理解其做用等同于cookie中的session_id,有该令牌就默认用户已经认证登录过。html

  • Signture 客户端签名

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

signture = md5(nonce + timastamp + salt)
复制代码

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

  • version 客户端版本

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

  • timestamp 请求时间戳

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


客户端签名细节

  • 防止重放攻击

虽然signture的存在,使咱们能够判断该请求是不是客户端生成,从而只响应客户端的请求。可是这样安全性仍是不够。git

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

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

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

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

服务端保证对每个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位的时间戳,它也要进行截取。不如直接提交过来怎么使用,服务端本身决定的。

<?php

// 13 位时间戳转10位 进行比对
time() - ceil($timestamp/1000);

// 若是是生成13位时间戳进行比对
list($micro,$time) = expload(' ',microtime());
ceil(($time + $micro)*1000) - $timestamp;

// 更新 其实microtime()是能够直接返回一个float数据,只须要传一个常数true
ceil(microtime(true)*1000) - $timestamp
复制代码

请求错误日志

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后端开发工具推荐

接口调试神器,发起一个http请求

抓包神器,能够抓APP发送过来的请求,查看是否有请求提交的参数都是什么

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

手机模拟器,能够在电脑上模拟多个Android系统的手机

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


以上就是我作APP后端开发的一些总结,因为是第一次开发APP后端,水平有限,还请你们多多指教

相关文章
相关标签/搜索