1 前言
前文提到过,要为设备生成一个唯一标识符,好像有很多思路,但是最佳实践,还是Uuid+KeyChain方案.
本文就对此方案进行具体的阐述.
首先,明确一下使用Uuid+KeyChain能做到什么?
使用Uuid+KeyChain可以做到:
1,应用在安装前获取到的设备标识符是A,删除后重新安装,获取的标识符还是A.
2,在一组应用内,共享一个标识符.同组的应用1,获得的设备标识符是A,应用B获取的设备还标识符还是1.
3,只要不手动进行删除,设备标识符不会变化.
2 了解钥匙链和访问组
2.1 钥匙链
钥匙链,是iOS设备的一个加密数据库,用来存储用户少量的隐私数据.
要操作钥匙链,需要导入安全框架:
#import <Security/Security.h>
并且相关操作,都是C函数.
但是我们不需要害怕C函数,只要熟悉之后,就会觉得其实还是挺简单的.
2.2 钥匙链项目
钥匙链项目是存储在钥匙链中的数据的基本单位.
如果我们想存储一个数据UserData,我们还需要提供给这个数据额外的一些属性Attributes字典,这些属性告诉系统如何存储和使用要存储数据.系统会根据这些属性,将UserData封装为一个钥匙链项目,然后存储到钥匙链中.
然后我们从钥匙链中查询数据的时候,也是通过提供一个Atrributes字典,来告诉系统如何从钥匙链中查找数据,以及数据返回的格式.
2.3 钥匙链访问组
访问组的概念,是钥匙链服务中,比较核心的概念.
访问组,是用字符串名字来标识的.
属于同一个访问组内的应用可以共享钥匙链项目.
2.4 应用的默认访问组.
每个应用,默认都被放置在以AppId命名的默认访问组中.
在我们什么也不操作的情况下,Xcode会将我们的应用配置到名字为TeamId.BundleId的访问组中,TeamId.BundleId即为AppId.
假设TeamId为zxc123456,BundleId为:com.example.AppOne,那么Xcode自动帮我们把应用加到了名字为zxc123456.com.example.AppOne的访问组中.
如果再创建一个应用,BundleId为:com.example.AppTwo,那么它就在zxc123456.com.example.AppTwo访问组中.
在这种情况下AppOne和AppTwo分别在两个不同的访问组中,它们之间无法共享数据.也就是说,我用AppOne存储一个标识符,AppTwo并不能获取到.
2.5 把应用添加到共同的访问组.
每个应用可以属于多个访问组,他们默认被放置在以自己AppId为名字的访问组中.
如果要增加访问组,需要手动配置.
有两种方式新增,一种是增加KeyChainSharing功能,一种是增加AppGroup功能.
AppGroup是比KeyChainSharing更加高级的共享,它除了能够共享钥匙链项目以外,还能共享NSUserDefault等数据.
这里,我们只讨论KeyChainSharing功能.
-
点击Capability中的加号:
-
然后选中KeyChainSharing
-
在Keychain Sharing中的Keychain groups中,添加任意数据,比如com.ShareItem
-
查看Xcode自动为我们创建的权限文件,内容多了一条:
Xcode自动在我们配置的数据前面,又加上了TeamId的前缀,即AppOne现在属于两个访问组: TeamId.com.ShareItem 和 TeamId.com.example.AppOne.
用同样的方式,把AppTwo也添加进来,这样这两个应用就都在TeamId.com.ShareItem中了,这样他们就可以共享钥匙链项目了.
2.6 每个钥匙链项目只属于一个钥匙链访问组
每个钥匙链项目在新增的时候,需要指定一个钥匙链访问组.
不同于应用,一个项目只能属于应用所在的过个访问组中的其中一个.
假如一个应用加入了3个访问组,那么该应用创建的钥匙链项目只能属于这3个的其中一个.
-0
然后每个应用只能获取到该应用加入的所有访问组中的钥匙链项目.
在上一步中,我们把两个应用都拉入了AppId.com.ShareItem,那么我们在App1中创建一个钥匙链项目,并指定为访问组为AppId.com.ShareItem,然后App2因为也在该访问组中,所以能获取到该项目,这样,就实现了钥匙链项目的共享.
3 实现共享设备标识符
3.1 获取Uuid
获取Uuid有两种方式:
//方式一:
CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);
NSString *cfuuidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));
//方式二:
NSString *uuid = [[NSUUID UUID] UUIDString];
3.2 将数据存储到Keychain
先上方法:
+ (void)saveContextToKeyChain:(NSString *)context forService:(NSString * _Nullable)service accessGroup:(NSString * _Nullable)accessGroup{
//钥匙链项目中kSecValueData中必须保存NSData.
NSData * data = [context dataUsingEncoding:NSUTF8StringEncoding];
//添加查询字典
NSMutableDictionary * mdic =[@{
//指定项目要保存的内容.
(NSString *)kSecValueData:data
//指定项目的类型
,(NSString *)kSecClass:(NSString *)kSecClassGenericPassword
} mutableCopy];
if(service) {
mdic[(NSString *)kSecAttrService] = service;
}
if(accessGroup) {
mdic[(NSString *)kSecAttrAccessGroup] = accessGroup;
}
//新增.
OSStatus status = SecItemAdd((CFDictionaryRef)mdic, nil);
if(status == errSecSuccess) {
NSLog(@"保存数据到KeyChain,成功,数据为:%@",context);
}
else {
NSString * errorInfo = (NSString *)CFBridgingRelease(SecCopyErrorMessageString(status, nil));
NSLog(@"保存数据到KeyChain,失败,原因为:%@",errorInfo);
}
}
需要注意以下几点:
-
对Keychain的操作,增删改查,都需要提供一个查询字典.
-
对于新增操作,需要提供一个新增查询字典.
该字典中,需要至少包含3个键值对.
kSecValueData键,用来指定要保存的数据,必须转化为NSData类型.
kSecClass键,用来指定要生成的钥匙链类型,这里值设为kSecClassGenericPassword是比较适合的.
kSecAttrService键,用来指定钥匙链的服务类型,这里指定是为了提供一个查找的条件,这个值可以任意输入.
kSecAttrAccessGroup键,用来指定访问组的名字.这个值是不能随便设置的.如果不设置,他是会添加到默认的访问组里的.一旦我们手动指定了访问组,这个指定的访问组就是 默认的访问组,所以这个值留空不填就可以了.
如果填了一个错误的值,比如填了一个值,但是应用并不在该访问组中,就会报错. -
新增的关键函数是SecItemAdd,返回值是OSStatus的状态码.errSecSuccess表示创建成功.
3.3 从keychain中获取指定值的钥匙链项目
以下方法将从KeyChain中查询出来的数据打印出来.
+ (void)logContextFromKeyChainForService:(NSString * _Nullable)service accessGroup:(NSString * _Nullable)accessGroup {
NSMutableArray * marrResult = [NSMutableArray array];
//搜索查询字典
NSMutableDictionary * searchQuery =[self searchQueryDictionaryForService:service accessGroup:accessGroup isSingleMatch:NO isReturnData:YES isReturnAttributes:YES];
CFTypeRef result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchQuery, (CFTypeRef *)&result);
if(status == errSecSuccess) {
//指定的是返回多个结果,所以结果是数组.
NSArray * arrResult = (__bridge NSArray *)result;
[arrResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSDictionary * dic = obj;
NSData * data = dic[(NSString *)kSecValueData];
NSString * value = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSString * service = dic[(NSString *)kSecAttrService];
NSString * accessGroup = dic[(NSString *)kSecAttrAccessGroup];
NSLog(@"value is %@,service is %@,accessGroup is %@",value,service,accessGroup);
}];
NSLog(@"KeyChain查询数据,成功,一个有%zi个项目",marrResult.count);
}
else if(status == errSecItemNotFound) {
NSLog(@"KeyChain查询数据,成功,没有匹配的钥匙链项目.");
}
else {
NSString * errorInfo = (NSString *)CFBridgingRelease(SecCopyErrorMessageString(status, nil));
NSLog(@"KeyChain查询数据,失败,原因为:%@",errorInfo);
}
}
//searchQuery的字典
+ (NSMutableDictionary *)searchQueryDictionaryForService:(NSString *)service accessGroup:(NSString * _Nullable)accessGroup isSingleMatch:(BOOL)isSingleMatch isReturnData:(BOOL)isReturnData isReturnAttributes:(BOOL)isReturnAttributes{
NSMutableDictionary * searchQuery =[NSMutableDictionary dictionary];
//指定项目的类型,必填项.
searchQuery[(NSString *)kSecClass]=(NSString *)kSecClassGenericPassword;
//返回的结果数量
if(isSingleMatch) {
searchQuery[(NSString *)kSecMatchLimit]=(NSString *)kSecMatchLimitOne;
}
else {
searchQuery[(NSString *)kSecMatchLimit]=(NSString *)kSecMatchLimitAll;
}
//是否返回项目的数据
if(isReturnData) {
searchQuery[(NSString *)kSecReturnData]=(id)kCFBooleanTrue;
}
else {
searchQuery[(NSString *)kSecReturnData]=(id)kCFBooleanFalse;
}
//是否返回项目属性
if(isReturnAttributes) {
searchQuery[(NSString *)kSecReturnAttributes]=(id)kCFBooleanTrue;
}
else {
searchQuery[(NSString *)kSecReturnAttributes]=(id)kCFBooleanFalse;
}
if(service) {
searchQuery[(NSString *)kSecAttrService] = service;
}
if(accessGroup) {
searchQuery[(NSString *)kSecAttrAccessGroup] = accessGroup;
}
return searchQuery;
}
注意以下几点:
- 在这个案例中,是通过servcie字段来区分要存储的内容.
- accessGroup字段为空就行,keychain会从所有的访问组中查找service字段匹配的数据.
4 示例代码说明
- 示例代码实现了对钥匙链项目的基本的增删改查操作.
- 对KeyChain项目的数据进行了简单的封装.
- 保存的数据在应用删除也不会丢失,再次安装后,自动恢复.
- 在进行了2.5步骤的操作的所有应用之间可以实现钥匙链数据共享.
Demo的地址为:
https://github.com/GikkiAres/GaKeyChainManager
网友评论