背景
在不需要用户登录或者没有用户概念的移动互联网产品中(如百度,今日头条,浏览器等),设计一种稳定的能够唯一标识客户设备的id(CUID),相当于移动互联网的用户 cookieid(匿名用户id),使得客户端自身以及和服务端交互时所产生的每一条行为日志的都能够找到对应到相应的设备标识,方便追踪和分析用户。
理论上每台终端机器都有一个唯一的设备号,如 Android 的 IMEI(International Mobile Equipment Identify),IOS 的 IDFA/IDFV。但是实际使用中会遇到各种各样的问题。
Android平台: 因为各种『中国特色』场景,并引发各种终端属性不靠谱的问题
- 山寨机: 一批或多批出货的手机终端属性可能都相同,IMEI 重复无法区分; 山寨机比例 5%+
- 双卡机: 获取到的终端属性混乱,APP获取卡槽优先级可能不同,有的获取到空卡槽位 SIM 信息,有的可能第一次取到卡槽1信息,第二次取到卡槽2信息
- 渠道刷机: 会修改同一终端的各种属性来达到虚假刷量目的,甚至使用终端模拟器生成假的 IMEI,MAC 等信息
- 刷ROM: 更新 ROM 后终端属性可能发生改变,如:IMEI,机型都被替换成 ROM 中的信息,MAC 被重新激活篡改
- 人为修改: 存在各种可能性的修改,如: 为了归避部分产品对同一个终端上重复注册的校验而修改 IMEI, 无法 WIFI 上网而修改 MAC 等, 换卡改变 SIM 卡信息等
- 终端属性获取异常: 如卡槽无 SIM 卡,根据业务数据分析结果 IMSI 能成功上报比例约 80%;WIFI 未启用时无法获取 MAC,成功获取上报的比例小于 70%; IMEI 也有约 2%~5% 的比例上报出错(开机初始阶段,获取 IMEI 最容易失败)
总结
归纳总结为『山寨机』和『刷机』两类问题:理论上使用 IMEI 可区分终端用户,但实际存在『不同终端IMEI重复』(典型场景:山寨机) 和『同一终端IMEI被修改』(典型场景:双卡机/刷机/刷ROM) 两类情况。
iOS平台: 终端用户唯一标识更因苹果公司的策略限制变得不可靠, 业界开源解决方案在用户标识的持久性上也不够给力
- UDID: Unique Device Identifier
- Deprecated in IOS 5,iOS 6 开始已被禁用
- 现在已经退化为UUID,每次获取的值都不同。
- MAC: iOS 7 中封杀 MAC 地址,之前的方法获取到的 MAC 地址全部都变成了02:00:00:00:00:00
- IDFA: IdentifierForAdvertising
- 一个跟device相关的唯一标识符,可以用来打通不同app之间的广告
- 它的目的是同一设备下的不同app信息共享
- 仅在 >= iOS 6 的版本中有效
- 用户手动重置(设置-通用-还原-还原位置与隐私);用户重置系统;用户关闭广告跟踪(设置-隐私-广告-限制广告跟踪)
- IDFV: IdentifierForVendor
- 仅在 >= iOS 6 的版本中有效
- 它的目的是同一开发商下的不同app信息共享。也就是说它只保证每个设备在所属同一个Vender的应用里,具有相同的值
- 重装或升级系统后都会重新产生;如果用户将属于此Vender的所有App卸载,则idfv的值会被重置,即再重装此Vender的App,idfv的值和之前不同
- 产品自己的GUID: 除了私有性外, 重点会因重装程序、重装系统、升级系统而丢失,服务器会重新生成导致用户虚高
- 开源OpenUdid: 在重装、升级系统后会下发新的ID,导致用户虚高。如升级 iOS7 的用户将获取到一个新的 OpenUdid,这部分升级用户就会以新增用户计入,导致数据虚高。
总结
苹果设备ID禁用风险。
解决方案
这个问题的难点其实不在于唯一性,而在于稳定性。随机构建一个唯一的设备/用户id,其实是一个非常简单的事情,只要把随机因子(如机器ip,线程id,时间戳)进行一个简单的运算(如md5),就可以得到一个唯一的id了(或者直接调用系统的 UUID 接口)。但是关键是要像身份证号码一样,要保证这个设备/用户一直都是使用这个id,在客户端版本的迁移过程中,必须保留不变。否则当用户进行版本升级后将会出现数据丢失现象,更好的情况就算是用户卸载安装了不同的版本,后台也能把这个用户的客户端的信息关联到找这个用户关联的id。
那么怎么解决这个问题呢?
有两种思路。
一种思路是每次都生成一样的id。只要输入是稳定的,算法也是固定的,那么生成的id就肯定是固定的。比如 md5(imei)
就是一种做法。
这个方法本来应该是完美的解决方案,但是逻辑上有个悖论:如果输入因子是稳定的,那么直接用输入因子作为用户id不就可以了,何必再用固定的算法重新生成一个?
第二种思路是随机生成唯一id,但是尽量保证同一个设备只会生成一次,这样就可以保证稳定性了。唯一的id前面说过其实并不难,最简单的使用UUID就可以了。那么如何保证只生成一次呢?做法就是把第一次生成得到的 uuid 保存到一个有权限读写但是又不容易被清除(比如卸载应用就会自动清理)的地方。比如 IOS 的话,一般会保存在 keychain 中。安卓则一般存储在 SD 卡中。SD 卡中以隐藏文件 backups/.SystemConfig/.cuid
保存,使用 AES-128-CBC
加密。
业界方案
腾讯
腾讯无线运营部有个「灯塔移动终端统一身份体系」项目是为解决移动终端设备唯一标识的一套完整技术方案,支持 Android、iOS 等主流平台,并为复杂移动终端环境下追踪非登录类 APP 用户提供了一种较业界更准确、稳定的终端用户身份ID:QIMEI。
腾讯手机QQ浏览器则是采用服务端随机分配 GUID 的方式。
百度
百度内部则统一使用 CUID 来唯一标识移动终端用户。其中 CUID 的生成策略依赖于一些设备信息。
andriod
deviceid|imei
逆序,其中deviceid = md5(imei + androidID + java.util.UUID)
IOS
md5(IDFV) + keychain
如果 keychain 中已经记录了 cuid,会一直读 keychain 记录的值作为 cuid,否则将 cuid 记到 keychain 中。
因为 IDFV 在用户卸载了所有相关 Vendor(Appid的域名,如:com.baidu.XXX)应用后会改变,所以用户在卸载了所有百度系应用后,每次执行一次生成函数 CUID 都有可能不同(因为在 iOS10.3 beta 2 版本,苹果改变了 keychain 的存储策略,当同一开发者账号下的所有APP被删除,keychain 存储的数据也会被清除) 因此,为了保证唯一性。当用户第一次生成 CUID 后,程序会将 CUID 存储到系统的 KeyChain 中。KeyChain 的存储与系统的开发者信息相关,一旦存储成功就不会再次执行生成函数,保证了 CUID 的唯一性。
Android 也是类型的,都是通过应用在终端缓存 CUID 来避免二次运行 CUID 生成算法来保证 ID 的稳定性。
但是这种方式存在变更风险。例如 IOS 在以下情况下就可能会产生 CUID 变更:
- 系统大版本升级,且没有通过 iTunes 备份系统(最大可能性)
- 对于一个没有安装过正式版百度地图的手机,安装渠道版本会写入 KeyChain 失败。因此,在这种情况下,每一次安装渠道包 cuid 都会发生变化
- 用户的 IDFV 获取失败,且 KeyChain 写入失败,这种情况会用随机数生成 CUID(可能性小,随机算出的 CUID 每次都会不一样)
思考
笔者没有 Get 到为什么 CUID 的生成需要获取 IMEI、AndroidID、IDFV 等信息,只要保证 CUID 第一次生成具有唯一性就可以了,而保证唯一性根本不需要这些输入因子,直接 UUID 不是最简单有效的方式吗?
一些术语说明
1、UDID
一个40字符的唯一字串,一个设备对应一个UDID,例如5f9d473fa45e5c68be836dc63fef6e8700000000)。IOS6之后禁止使用。
禁用之后苹果给出了替代方案(此方法最终生成一个由连字符组合在一起的32字符,包含连字符一起是36个字符,CFUUIDCreate本身生成的值是128-bit,其算法是以以太网的硬件地址和以1582.10.15.0:0:0算起的100纳秒的数量组合形成的)的唯一字串,形如:68753A44-4D6F-1226-9C60-0050E4C00067。
但是此方法每次调用都会生成一个新值,不对应一个设备。而且iOS 7之后MAC地址获取也被禁用,之前的方法获取到的MAC地址全部都变成了02:00:00:00:00:00。
2、OpenUDID
OpenUDID是由Yann Lechelle(Appsfire创始人之一)在11年8月28日建立的一个开源项目,旨在创建一个设备唯一标识(UDID)此标识不因app的删除而改变。
其实现原理是:利用苹果推荐的UUID生成方法生成一个UDID(格式类似于:a633884c144d873504cdf379f21afd71bfa2eb0a),生成之后分别存在app对应的存储空间(NSUserDefaults)和一个剪切板中(每个应用会创建自己的剪切板用于保存数据,经验证重启后剪切板数据不丢失),当OpenUDID每次生成时会先取这两个值(剪切板数据每个App共享,每个应用均会创建一个剪切板保存此UDID)从而达到每个App、App删除后重新安装生成的UDID均一样。
OpenUDID利用了一个非常巧妙的方法在不同程序间存储标示符:在粘贴板中用了一个特殊的名称来存储标示符。通过这种方法,别的程序(同样使用了OpenUDID)知道去什么地方获取已经生成的标示符(而不用再生成一个新的)。
风险:每台iOS设备的OpenUDID是通过第一个带有OpenUDID SDK包的App生成,如果你完全删除全部带有OpenUDID SDK包的App(比如恢复系统等),那么再次调用生成函数将会生成一个不一样的UDID,相当于新设备。目前所有使用OpenUDID的App在github上有介绍。
为了解决所有使用OpenUDID的App全删除会使得生成的UDID不一样的问题,可以考虑将生成的UDID保存到keychain中(keychain不会因为App的删除或系统升级而删除,可以在同一SEEDID下的所有App之间共享,但是在iOS的系统还原设置中抹掉所有内容和设置后会清空keychain内的内容),优先使用keychain中的UDID。
3、idfa(advertisingIdentifier)
优点:不限于同一开发者账号共享 缺点:用户关闭广告追踪就获取不到此ID;用户可以手动还原重置ID。(系统“设置》隐私》广告”里限制广告跟踪和还原标识符)
4、idfv(identifierForVendor)
优点:同一开发者账号的APP可以共享ID 缺点:同一开发者账号下的APP被删除再重新安装,ID会重新生成