赞
踩
苹果对用户隐私方面的权限管理非常严格,不允许调用私有API获取用户硬件的相关的ID,其中包括手机号、UDID、IMIE、序列号、MAC地址等,这些能解析设备唯一性的信息都不能获取,否则无法上架App Store.本文将讨论如何获取设备的相关ID,同时不违背苹果对于隐私管控的规定。
1. UDID与设备ID
UDID的全称是Unique Device Identifier,它是iOS设备的唯一标识码,由40位十六进制的字母和数字组成。在iOS5以下,苹果的开发者们通常使用UDID作为设备的唯一标识,我们可以通过调用私有API的方式获得UDID,代码如下:
NSString *udid = [[UIDevice currentDevice]uniqueIdentifier];
除了通过调用私有的API获取UDID之外,还可以通过Safari浏览器安装描述文件来获取UDID,访问http:www.exchen.net/udid,然后点击“获取UDID”按钮,会出现描述文件的安装提示,安装好描述文件以后,页面上会显示UDID。
关于如何通过safari浏览器获取UDID,详情可以参阅https://www.exchen.net/通过-safari-浏览器获取-udid.html。
2. IDFA
对于一个设备上的所有应用,获取的IDFA都是一样的。由于苹果取消了UDID的获取方法,IDFA就成了一种标识和追踪用户的常用方法。一台新的iOS设备在激活之后,IDFA是默认开启的,用户也可以关闭或者重置,如果重置,应用再次获取IDFA就会有变化,但是用户一般是不知道这个开关和设置的。IDFA的获取代码如下:
#import <AdSupport/AdSupport.h>
NSString *strIDFA = [ASIdentifierManager sharedManager]advertisingIdentifier]UUIDString];
NSLog(@"IDFA: %@",strIDFA);
输出结果如下:
2020-04-20 15:33:46.831800+0800 DeviceUDID[1461:137700] IDFA: 3D42C428-513E-44EA-AB82-BC49DD641286
如果在 “设置” -> “隐私” -> “广告” 中开启“限制广告追踪”,就会关闭IDFA,如图所示,此时获取的IDFA的值就会变成00000000-0000-0000-0000-000000000000。如果点击“还原广告标识符”,那么再次获得的IDFA就和上次是不一样的。
3.IDFV
IDFV是供应商标识符,当同一个证书签发的多个应用安装在同一个设备上时,这些应用获取的IDFV都是一样的。不论卸载了几个应用,只要设备上还存在该证书签发的应用,重新安装的证书签发的其他应用,IDFV就不会变。如果将该证书签发的应用全部卸载,重新安装的应用所获取的IDFV就会变化。获取IDFV的代码如下:
NSString *strIDFV = [UIDevice currentDevice]identifierForVendor]UUIDString];
NSLog(@"strIDFV:%@",strIDFV);
运行后输出的结果:
2020-04-20 17:29:45.531076+0800 DeviceUDID[2356:199871] strIDFV: 0E07AAE2-3EAA-4011-846F-2793D10C7C1F
4.OpenUDID
OpenUDID是一种开源的ID生成算法,下载地址为:https://github.com/ylechelle/OpenUDID,使用OpenUDID的代码如下:
#import "OpenUDID.h"
NSString *strOpenUDID = [OpenUDID value];
NSLog(@"strOpenUDID:%@",strOpenUDID);
打印数据:
2020-04-21 09:02:11.556993+0800 DeviceUDID[17351:44308] strOpenUDID: 25f81fe2fae5cb67ef817322baf9a7803f53f2d7
由于高版本的Xcode默认启用ARC,而早期的OpenUDID出现的时候还没有ARC模式,所以编译时要注意,在Build Phases里的Compile Sources中,给OpenUUID.m添加-fno-objc-arc标记来关闭ARC,如图所示。
OpenUUID的核心原理是将生成的ID保存到剪切板中,同时在沙盒的Preferences目录下也保存一份,当应用被卸载时,沙盒目录会清空,但是剪切板中的数据还存在,所以从剪切板中获取数据OpenUUID就可以了。
下面我们来看一下OpenUUID内部实现的代码:
5. simulateIDFA
2016年,有米公司开源了一个获取ID的方法simulateIDFA,该方法可以在短时间内识别一台设备的唯一性,主要用于进行广告检测。由于一些设备的IDFA被关闭,所以simulateIDFA就可以成为一种“曲线救国”的方法。下载地址为: https://github.com/youmi/SimulateIDFA,调用方法也很简单,只需要调用createSimulateIDFA函数即可,代码如下:
#import "SimulateIDFA.h"
NSString *simulateIDFA = [SimulateIDFA createSimulateIDFA];
NSLog(@"simulateIDFA: %@",simulateIDFA);
2020-04-21 10:03:46.321838+0800 DeviceUDID[17819:78121] simulateIDFA: 7AA3216A-905E-1F0B-9EE0-8CF164A6ABCA
createSimulateIDFA函数获取ID的原理:首先获取两组信息,一组是不稳定的信息,包括系统启用时间、国家代码、本地语言、设备名称,另一组是稳定的信息,包括系统版本、机型、运营商名称、内存、coreServices文件创建时间、硬盘使用空间;然后将这两组获取到的信息都格式化为字符串后放到fingerPrintUnstablePart和fingerPrintStablePart变量中;接着对这两组信息进行MD5加密,最后将加密后的两组值通过combineTwoFingerPrint函数转化为与IDFA一样的格式。SimulateIDFA的核心代码如下:
6. Mac地址
除了使用UDID作为iOS设备的唯一标识符,还可以使用MAC地址。调用sysctl和ioctl可以获取MAC地址,直到iOS7的出现。不知出于什么原因,苹果对于sysctl和ioctl进行了技术处理,让MAC地址返回02:00:00:00:00:00。官方文档上这样写的:
“Two low-level networking APIs that used to return a MAC address now return thefixed value 02:00:00:00:00:00. The APIs in question are sysctl(NET_RT_IFLIST) and ioctl(SIOCGIFCONF). Developers using the value of the MAC address should migrate toidentifiers such as -[UIDevice identifierForVendor].This change affects all apps running on iOS 7”
原文的意思是苹果对于sysctl和ioctl进行了技术处理,Mac地址返回的都是 02:00:00:00:00:00,有需要使用MAC地址的开发者请使用其他方法,如[UIDevice identifierForVendor].这个改动会影响iOS7及以上系统。
由于iOS7无法获得MAC地址,所以不得不另谋出路。AppStore上有一款应用叫做Fing,该应用是一款网络扫描器,能扫描到局域网的主机和MAC地址,其中包含本机的MAC地址。于是对该应用进行分析,发现Fing是从ARP列表中获取的MAC地址,这是一个很好的思路,下面我们来实现一下,其功能包括本机的MAC地址,获取路由器的IP地址,获取路由的MAC地址.
7. ID的持久存储
众所周知,每个iOS应用在系统上都对应一个沙盒,它只能在自己的沙盒中存储数据,如果应用被卸载,那么其沙盒目录也会被删除,就像微信被卸载了,那么本地的聊天记录肯定也就没有了。由于UDID不能获取,所以应用开发者只能自己生成一个ID,想要这个ID不变,就必须持久化的数据存储。在iOS系统上有两个地方可以做到应用被卸载后,数据不会被清理,一个是Keychain钥匙串,还有一个是剪切板。而之前我们所介绍的OpenUUID就是利用剪切板存储的。
1. Keychain存储
苹果封装了一个读写Keychain的对象keychainItemWrapper,将该对象的源代码文件添加到工程中。需要注意的是,在Build Phases里,在Compile Sources里的KeychainItemWrapper.m中,添加Compiler Flags为-fno-objc-arc来关闭ARC,如图所示
KeychainItemWrapper.m关闭ARC模式
这里定义了一个函数getIDForKeychain,通过这个函数能获取和生成ID,代码如下:
NSString *keychainID = [self getIDForKeychain];
下面来看看getIDForKeychain函数的过程,其代码如下:
//获取和生成一个ID并用keychain进行存储
-(NSString *)getIDForKeychain{
NSString *strBundleSeedID = [self bundleSeedID];
NSDictionary *infoDic = [[NSBundle mainBundle]infoDictionary];
NSString *strAppname = [infoDic objectForKey:@"CFBundleIdentifier"];
NSString *strGroup = [NSString stringWithFormat:@"%@.%@",strBundleSeedID,strAppname];
NSLog(@"strGroup:%@",strGroup);
KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc]initWithIdentifier:@"super" accessGroup:strGroup];
NSString *strValue = [keychainItem objectForKey:(NSString *)CFBridgingRelease(kSecValueData)];
if ([strValue isEqualToString:@""]||strValue==nil) {
//随机生成ID
CFUUIDRef uuid = CFUUIDCreate(NULL);
strValue = (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, uuid));
strValue = [self md5:strValue];//MD5
[keychainItem setObject:strValue forKey:(NSString *)CFBridgingRelease(kSecValueData)];
}
return strValue;
}
首先,要获取bundleSeedID,也就是证书前缀,将证书的前缀和应用的CFBundleIdentifier格式化为一个group.这个group很重要,因为应用之间读取keychain也是互相隔离的,必须要在同一个访问组才能读取。然后调用[keychainItem objectForkey]获取对应的keychain中的值。如果获取的数据为空,说明是第一次运行,需要生成ID,其方法是调用CFUUIDCreateString函数生成一个随机的UUID串,然后再进行一次MD5加密,接着调用[keychainItem setObject]将ID写入keychain,最后返回ID。
bundleSeedID获取证书的前缀的函数如下,其实际原理也是从keychain中读取:
-(NSString *)bundleSeedID{
NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:
(NSString *)CFBridgingRelease(kSecClassGenericPassword),kSecClass,
@"bundleSeedID",kSecAttrAccount,
@"",kSecAttrService,
(id)kCFBooleanTrue,kSecReturnAttributes,
nil];
CFDictionaryRef result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&result);
if (status==errSecItemNotFound) {
status = SecItemAdd((CFDictionaryRef)query, (CFTypeRef *)&result);
}
if (status!=errSecSuccess) {
return nil;
}
NSString *accessGroup = [(__bridge NSDictionary *)result objectForKey:(NSString *)CFBridgingRelease(kSecAttrAccessGroup)];
NSArray *compents = [accessGroup componentsSeparatedByString:@"."];
NSString *bundleSeedID = [[compents objectEnumerator]nextObject];
CFRelease(result);
return bundleSeedID;
}
2. 剪贴板
除了Keychain能够进行持久化的数据存储外,剪贴板也可以。我们将ID保存在剪贴板中,如果应用被卸载了,下次安装依然能够获取到,并且在系统升级更新时也保持不变。下面定义了一个getIDForPasteboard函数,通过这个函数获取和生成ID。
NSString * pasteboardID = [self getIDForPasteboard];
下面我们来看看getIDForPasteboard函数的实现过程,代码如下:
-(NSString *)getIDForPasteboard{
NSString *pasteboardName = @"exchen.net";
NSString *pasteboardType = @"id";
NSString *strID;
UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:pasteboardName create:YES];
pasteboard.persistent = YES;
//从剪贴板里读取ID
id item = [pasteboard dataForPasteboardType:pasteboardType];
if (item) {
//如果读取的数据不为空,说明之前就写入了ID
item = [NSKeyedUnarchiver unarchiveObjectWithData:item];
if (item!=nil) {
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:item];
strID = [dic objectForKey:pasteboardType];
}
}
else{
//随机生成ID
CFUUIDRef uuid = CFUUIDCreate(NULL);
strID = (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, uuid));
strID = [self md5:strValue];//MD5
//写入剪贴板
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:item];
[dic setValue:strID forKey:pasteboardType];
[pasteboard setData:[NSKeyedArchiver archivedDataWithRootObject:dic] forPasteboardType:pasteboardType];
}
return strID;
}
首先,定义剪贴板的名称和类型,调用[UIPasteboard pasteboardWithName]创建剪贴板,通过dataForPasteboardType获取相应类型的数据。如果能够读到内容,就调用[NSKeyedUnarchiver unarchiveObjectWithData:item]将数据解析出来,然后返回ID;如果读不到内容,就说明是第一次运行,需要生成ID。生成ID的方法是调用CFUUIDCreateString创建一个随机的UUID,接着对UUID进行MD加密。ID生成之后,调用setData向剪贴板中写入数据,最后返回ID。
8. DeviceToken
DeviceToken主要用于推送消息,也可以识别用户的设备,其获取方法是在AppDeleagte.m里添加didRegisterForRemoteNotificationsWithDeviceToken方法,具体代码如下:
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
NSString *strDeviceToken = [[[[deviceToken description]stringByReplacingOccurrencesOfString:@"<" withString:@"."]stringByReplacingOccurrencesOfString:@">" withString:@""]stringByReplacingOccurrencesOfString:@" " withString:@""];
NSLog(@"DeviceToken:%@",strDeviceToken);
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。