通讯录同步及好友匹配问题总结

猪小花1号2018-09-07 09:33

作者:刘魏威


通讯录同步,这里仅指手机客户端与服务端之间的单向同步,不讨论syncML等同步协议。本篇文章是开发通讯录数据同步和好友匹配过程中的问题总结。

同步方式的选择

同步方式的选择关系到开发成本、服务稳定性及用户体验,因此需要慎重考虑。

全量同步

明显的好处是客户端开发简单,每次都直接上传全量数据,其他均交给服务端处理。然而客户端每次上传时,数据量可能比较大,网络传输会存在瓶颈,同时服务端处理时间因数据量的大小而不确定,服务很难承诺稳定,最终导致用户体验差。

假设我们以全量同步的方式去实现,服务端具体会面临哪些问题呢?下面来仔细分析下:

  • 数据量大,如果不分批处理,依赖的第三方服务接口、服务本身处理时间都会存在问题。而分批处理,又会涉及到状态同步,部分处理失败的问题。
  • 通讯录数据删除,服务端比较难识别。比较笨的方法是逐一遍历老的数据,在本次新上传数据中查找,但这种方法效率上会存在问题。另一种方法是每次上传都是认为是一个新的版本,存在的数据增加版本号,查询永远查最新版本,联系人删除的记录就不会查到了。然而此种方法也有一个明显的缺点,因为用户通讯录通常只会变更少量数据,而这种方式每次处理都会更新大量数据,导致数据库tps比较大。

增量同步

客户端每次只上传变更的数据,只有第一次需要全量上传。好处是只有第一次可能体验差,但后续的请求都能快速处理,服务端稳定性相对有所提高。

增量同步需要考虑脏数据的问题,可能由于某种原因同步失败,客户端丢失数据,服务端能够识别并要求客户端重新全量上传。

不论是全量同步还是增量同步,对于服务端来说开发量是一样的,每次上传,业务逻辑基本可以当做全量同步来处理。同步的接口可以开两个,方便做故障隔离及限流。传输数据格式定义上,尽量使用字符串拼接,不要用json,保证传输的数据量尽可能小。

基于设备还是账号

用户通讯录是基于设备的,即每个设备只会有一个通讯录。但对于一个app来说,在同一个设备可以使用多个app账号登陆的,因此服务端存储的通讯录是基于设备还是账号,需要考虑下。如果基于设备,设备id可以伪造,看起来不太安全。基于账号,那么同一个设备的通讯录可能在服务端被存储多次,数据重复,而且用户切换账号,需要全量上传,处理的逻辑较多。从用户体验的角度来看,基于设备比基于账号更合理。

数据安全问题

由于通讯录是属于比较私人的信息,一旦泄露后果不堪设想,因此通讯录数据需要有较高的安全等级。

数据安全主要从两个角度来看:

  • 传输是否安全
  • 存储是否安全

这两者在开发过程中都需要保证。

加解密算法的选择

数据加密算法主要有两类,对称加密和非对称加密,下面来分析如何做出选择。

对称加密 - AES 算法

比较常见的加密算法,服务端和客户端同时存储一份相同的密钥,只要密钥不暴露,即可认为数据是安全的。当前考拉实名认证,身份证信息加密传输就是使用的此种算法。这么做有什么缺点呢?

  • 从客户端来看,密钥写在代码里,无论如何都存在被破解的可能。另外密钥不能随便更改,始终会存在老版本的客户端使用旧的密钥。
  • 从服务端来看,使用java语言,由于jdk自带的加解密工具对于密钥长度有限制(jdk7默认只支持16字节长的密钥),如果需要支持32位,需要更新 jre/lib/secrutity 目录里的政策文件。网上提到开源的工具包如 Bounce Castle可以支持32位,但我试了后还是不行,最终通过反射的方式去修改jdk自带类的属性才解决。
非对称加密 - RSA 算法

服务端存储私钥,客户端存储公钥,不存在密钥泄漏的风险。但RSA算法对明文长度有要求,不能超过公钥长度,如果超过,需要对明文进行分段加密。

在实现过程中,可以综合两种算法的优缺点,实现一套相对安全的加解密方案:

1、客户端随机生成固定长度的对称密钥,使用此密钥来加密通讯录数据

2、使用RSA公钥来加密对称密钥,数据上传时,同此上传对称加密后的数据和非对称加密后的密钥

3、服务端收到数据,先用RSA私钥解密对称密钥,再用对称密钥解密通讯录数据

最终由于开发时间的限制,选了目前正在使用的AES加密算法来传输,客户端不用进行额外的开发,服务端也不用针对环境来提供不同的公钥。

通讯录数据存储和查询

通讯录数据存储使用的是通用AES加解密服务(特地针对社区提供批量加解密接口)。使用通用AES加解密服务时需要注意,由于系统会定期更换密钥,相同的明文在不同的时间会生成不同的密文。这一点会导致什么呢?如果你用加密后的数据来做业务逻辑判断,比如需要判断一个手机号在业务表(里面存储的手机号已经加密)里是否存在,先使用通用加密服务进行加密,然后去业务表里面查询,这个时候很可能查不到!因此,需要十分注意,加密后的数据,仅能用来解密,不能作于其他任何用途。如果需要以手机号作为业务唯一性用途,可以使用固定算法对手机号生成一个不可逆的特征码,查询时以特征码为准。

通讯录上传完后,客户端有查询通讯录匹配到的好友并展示联系人姓名的需求。如何防止联系人数据泄密?最主要的风险在于通讯录是基于设备id的,而设备id可以伪造,因此需要识别查询请求是否真正来自通讯录所属设备。可以设计一个请求签名:

request_sign = AES_Encrypt(last_data_sign,device_id,timestamp)

last_data_sign指客户端最后一次同步数据签名,存储在客户端本地,每次同步完数据,客户端需要保存下来。last_data_sign加device_id可以防止设备伪造,timestamp可以防止请求被拦截后重放。

数据分批处理

接收到客户端上传的数据,先做解密验证,数据有效则存储并生成异步任务,任务落db成功即返回,保证接口响应时间。如果是全量同步,由于上传完客户端需要马上查询,为了能保证查到数据,服务端同步前两页的数据,其他批次异步处理。异步任务因机器重启、网络故障等原因处理失败,需要有定时重试机制兜底。

由于存在重试机制,通讯录的增改删均需要实现幂等处理。

异常情况处理

通讯录上传->匹配好友->获取匹配结果,这个流程是一个整体,用户无操作上的感知,要么全部成功,要么全部失败。但实际上无法百分百保证流程中每一步都成功,为防止某一步失败导致功能不正常,客户端、服务端都需要有容错处理机制。

  • 接收到客户端上传数据后,服务端处理可能会遇到第三方依赖无法访问、应用重启等导致数据处理失败,此时客户端能支持重试,服务端对失败的任务也能识别并支持客户端重试
  • 客户端可能由于未知的原因,丢失获取匹配结果接口访问签名,为防止阻塞在匹配结果接口,客户端需要支持重新全量上传(当前APP提供了清除本地通讯录数据缓存功能,强制上传),服务端对于每一次全量上传,均校验数据签名和最近一次处理的签名是否一致,防止重复处理。
  • 客户端手机通讯录可能会有一个联系人对应上万个手机号的情况(比如搜狗号码通会把骚扰电话存进通讯录,以支持来电显示提醒用户),这种需要避免上传
  • 客户端有通讯录数据,但本地过滤完后是空的,没有必要进行上传,但后面需要访问获取匹配结果接口以获取其他信息(如邀请好友链接),因此匹配结果接口要支持无签名访问。
  • 客户端上传完通讯录后,服务端先过滤非法手机号,如果过滤完后是空的,不能中断流程,需要插入一条签名数据,防止后面客户端无法访问匹配结果接口。
  • 通讯录联系人数量计数需要基于登陆账号,由于一个设备会登陆多个账号,因此存储计数时需要对每个账号存储一份计数,设备通讯录数据变更时,需要同时更新设备关联的所有账号计数,基于redis hashmap实现会方便很多。
  • 通讯录姓名长度限制,数据库里存储的是加密后的字符串,而加密后长度和加密之前不一样,因此要提前计算好加密之前的字符最大长度。


网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者刘魏威授权发布