avatar

关于移动端设备唯一标识的解决方案

关于设备唯一标识解决方案

因为涉及到的名词以及英文缩写比较多,建议看的过程中,遇到不太熟悉的可以直接到名词解释下查看!

具体需求

跟踪用户层次:
每次用户的擦除设备、恢复出厂设置动作后,都将视设备为新设备。

跟踪硬件层次:
无论设备擦除数据或者恢复出厂设置后都需要将该设备视为同一台设备。

解决方案

Android解决方案

1- Android下的用户层次解决方案

因为Android_ID已经可以解决大多数机型的需求,但是具有缺陷,无法使用所有的用户。
所以将以Anroid_ID为主体的多个值来生成用户设备唯一ID。
与设备使用周期有关的Android_ID、SIM卡序列号、Device_ID
通过UUID或者是MD5实现加密计算。

DeviceID 是Android系统提供用于识别手机设备的串号,不同手机设备得到 IMEI、MEID、ESN
获取DeviceID需要READ_PHONE_STATE权限,需要加入考虑

2- Android下的硬件层解决方案

跟踪硬件层次上的设备建议使用硬件的标识符,比如设备ID(DeviceId)、Mac 地址、设备序列号(SN)或者设备的品牌,型号名等,这些值在用户擦除数据或者恢复出厂设置后也不会改变。
同样的,为了提升稳定性及排除单一标识符所存在的缺陷,使用多个标识符拼接,然后通过 UUID 或者 MD5 算法计算得出 我们需要的设备标识符。

IOS解决方案

1-IOS下的用户层次的解决方案

通过,获取IDFA或者IDFV得到用户的UUID。并且,同一个Vender来说这个值是一样的。
如果用户刷机或者重装系统之后,这个值还是会改变。
可以参考附录IOS解决方案核心代码

2-IOS下的硬件层次的解决方案

目前尚未找到解决方案,因为苹果限制了开发者的权限。

名词解释

关于IMEI、MEID、ESN、IMSI、Android_ID的区别

IMEI

全称 国际移动设备识别码( International Mobile Equipment Identificantion Number )
全球每一部手机的IMEI码都是唯一的,从生产到交付使用都将被制造生产的产商记录。

IMEI由15-17位数字组成,其组成为:
1、前6位数(TAC)是”型号核准号码”,一般代表机型。
2、接着的2位数(FAC)是”最后装配号”,一般代表产地。
3、之后的6位数(SNR)是”串号”,一般代表生产顺序号。
4、15位数(SP)通常是”0”,为检验码,目前暂备用。
5、最后两位常是不同产商记录的软件版本号。

MEID

全称 移动设备标识符( Mobile Equipment IDentifier )
全球每一部手机的MEID都是唯一的,是56bit的移动终端识别号。

MEID由14个十六进制数字标识,第15位为校验位,不参与空中传输。
RR:范围A0-FF,由官方分配
XXXXXX:范围 000000-FFFFFF,由官方分配
ZZZZZZ:范围 000000-FFFFFF,厂商分配给每台终端的流水号
C/CD:0-F,校验码

MEID前身是ESN,由于32bit的ESN不够用,才推出了56bit的MEID。目前的CDMA手机一般都具有ESN/MEID。

ESN

全称 电子序列号(Electronic Serial Number)
MEID的前身,32bit的设备标识,由于移动设备增多,发展为56bit的MEID。

MSI

全称 国际移动用户识别码(International mobile subscriber identification number)
区别移动用户的标志,储存在SIM卡中,可用于区别移动用户的有效信息。
其总长度不超过15位,全部由数字构成,并且分为三段 MCC + MNC + MSIN
MCC:移动国家代码(中国是460)
MNC:移动网络代码
MSIN:移动订户识别代码

Android_ID

在设备首次启动时,系统会随机生成一个64位的数字,并把这个数字以16进制字符串的形式保存下来,这个16进制的字符串就是ANDROID_ID,当设备被wipe后该值会被重置。
ANDROID_ID可以作为设备标识,但需要注意:

  1. 厂商定制系统的Bug:不同的设备可能会产生相同的ANDROID_ID。
  2. 厂商定制系统的Bug:有些设备返回的值为null。
  3. 设备差异:对于CDMA设备,ANDROID_ID和TelephonyManager.getDeviceId() 返回相同的值。

MAC物理地址

MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会发生变化。
一般不建议使用 MAC 地址进行任何形式的用户标识。可能是因为,在用户没有连接WIFI的情况下,MAC地址为空。
因此,从 Android M 开始,无法再通过第三方 API 获得本地设备 MAC 地址(例如,WLAN 和蓝牙)。
而IOS从7.0开始也无法获取MAC物理地址。

Installtion ID

程序安装后第一次运行时生成一个ID,该方式和设备唯一标识不一样,不同的应用程序会产生不同的ID,同一个程序重新安装也会不同。
所以这不是设备的唯一ID,但是可以保证每个用户的ID是不同的。可以说是用来标识每一份应用程序的唯一ID(即Installtion ID),可以用来跟踪应用的安装数量等。

UDID

全称 唯一设备标识符描述(Unique Device Identifier Description)

由子母和数字组成的40个字符串的序号,用来区别每一个唯一的iOS设备,包括 iPhones, iPads, 以及 iPod touches,这些编码看起来是随机的,实际上是跟硬件设备特点相联系的。
从IOS 2.0 开始苹果向开发者提供,但是从IOS 5.0 的时候就被废除了。

IDFA

全称 广告标识符(advertising Identifier)

直译就是广告id, 在同一个设备上的所有App都会取到相同的值,是苹果专门给各广告提供商用来追踪用户而设的,用户可以在 设置|隐私|广告追踪 里重置此id的值,或限制此id的使用,故此id有可能会取不到值,但好在Apple默认是允许追踪的,而且一般用户都不知道有这么个设置,所以基本上用来监测推广效果,是戳戳有余了。
IOS6.0以上

注意:由于idfa会出现取不到的情况,故绝不可以作为业务分析的主id,来识别用户。

IDFV

全称 标识符供应商(identifier For Vendor)

说明:顾名思义,是给Vendor标识用户用的,每个设备在所属同一个Vender的应用里,都有相同的值。其中的Vender是指应用提供商,但准确点说,是通过BundleID的反转的前两部分进行匹配,如果相同就是同一个Vender,例如对于com.taobao.app1, com.taobao.app2 这两个BundleID来说,就属于同一个Vender,共享同一个idfv的值。和idfa不同的是,idfv的值是一定能取到的,所以非常适合于作为内部用户行为分析的主id,来标识用户,替代OpenUDID。
IOS6.0以上

注意:如果用户将属于此Vender的所有App卸载,则idfv的值会被重置,即再重装此Vender的App,idfv的值和之前不同。

附录

IOS解决方案 部分代码
采用IDFV + SAMKeychain的解决方案

// 采用IDFV + SAMKeychain的解决方案
+ (NSString *)getIDFV
{
//定义存入keychain中的账号 一个标识 表示是某个app存储的内容 bundle id最好
NSString * const KEY_USERNAME = @"com.wdw.zzzz.username";
NSString * const KEY_PASSWORD = @"com.wdw.zzzz.password";

//测试用 清除keychain中的内容
//[IDFVTools delete:KEY_USERNAME_PASSWORD];

//读取账号中保存的内容
NSMutableDictionary *readUserDataDic = (NSMutableDictionary *)[IDFVTools load:KEY_USERNAME];
//NSLog(@"keychain==%@",readUserDataDic);

if (!readUserDataDic)
{//如果是第一次 肯定获取不到 这个时候就存储一个

NSString *deviceIdStr = [[[UIDevice currentDevice] identifierForVendor] UUIDString];//获取IDFV
//NSLog(@"identifierStr==%@",identifierStr);

NSMutableDictionary *needSaveDataDic = [NSMutableDictionary dictionaryWithObject:deviceIdStr forKey:KEY_PASSWORD];
//进行存储 并返回这个数据
[IDFVTools save:KEY_USERNAME data:needSaveDataDic];

return deviceIdStr;
}
else{return [readUserDataDic objectForKey:KEY_PASSWORD];}
}

//储存
+ (void)save:(NSString *)service data:(id)data
{
//Get search dictionary
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];

//Delete old item before add new item
SecItemDelete((__bridge CFDictionaryRef)keychainQuery);

//Add new object to search dictionary(Attention:the data format)
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(__bridge id)kSecValueData];

//Add item to keychain with the search dictionary
SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);
}

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service
{
return [NSMutableDictionary dictionaryWithObjectsAndKeys: (__bridge id)kSecClassGenericPassword,(__bridge id)kSecClass, service, (__bridge id)kSecAttrService, service, (__bridge id)kSecAttrAccount, (__bridge id)kSecAttrAccessibleAfterFirstUnlock,(__bridge id)kSecAttrAccessible, nil];
}

//取出
+ (id)load:(NSString *)service
{
id ret = nil;

NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];

//Configure the search setting

//Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue

[keychainQuery setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];

[keychainQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];

CFDataRef keyData = NULL;

if (SecItemCopyMatching((__bridge CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr)
{
@try
{
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
}
@catch (NSException *e)
{NSLog(@"Unarchive of %@ failed: %@", service, e);}
@finally
{}
}

if (keyData)
CFRelease(keyData);

return ret;
}

//删除
+ (void)delete:(NSString *)service
{
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
}

关于Android解决方案的验证情况

  • 开启WIFI与关闭WIFI情况下,获得的UUID一致
  • 重装APP前后获得的UUID一致
  • 关机重启前后获得的UUID一致
  • 定制系统升级前后获得的UUID一致(此处以小米系统为例)
文章作者: 不君子
文章链接: http://yoursite.com/2019/10/28/%E8%AE%BE%E5%A4%87%E5%94%AF%E4%B8%80%E6%A0%87%E8%AF%86/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 不君子