iOS | Uuid+KeyChain 让你的设备全球唯一

1 前言

前文提到过,要为设备生成一个唯一标识符,好像有很多思路,但是最佳实践,还是Uuid+KeyChain方案.

本文就对此方案进行具体的阐述.

首先,明确一下使用Uuid+KeyChain能做到什么?
使用Uuid+KeyChain可以做到:
1,应用在安装前获取到的设备标识符是A,删除后重新安装,获取的标识符还是A.
2,在一组应用内,共享一个标识符.同组的应用1,获得的设备标识符是A,应用B获取的设备还标识符还是1.
3,只要不手动进行删除,设备标识符不会变化.

2 了解钥匙链和访问组

2.1 钥匙链

钥匙链,是iOS设备的一个加密数据库,用来存储用户少量的隐私数据.
要操作钥匙链,需要导入安全框架:
#import
并且相关操作,都是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);
    }
}

需要注意以下几点:

  1. 对Keychain的操作,增删改查,都需要提供一个查询字典.

  2. 对于新增操作,需要提供一个新增查询字典.
    该字典中,需要至少包含3个键值对.
    kSecValueData键,用来指定要保存的数据,必须转化为NSData类型.
    kSecClass键,用来指定要生成的钥匙链类型,这里值设为kSecClassGenericPassword是比较适合的.
    kSecAttrService键,用来指定钥匙链的服务类型,这里指定是为了提供一个查找的条件,这个值可以任意输入.
    kSecAttrAccessGroup键,用来指定访问组的名字.这个值是不能随便设置的.如果不设置,他是会添加到默认的访问组里的.一旦我们手动指定了访问组,这个指定的访问组就是 默认的访问组,所以这个值留空不填就可以了.
    如果填了一个错误的值,比如填了一个值,但是应用并不在该访问组中,就会报错.

  3. 新增的关键函数是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;
}

注意以下几点:

  1. 在这个案例中,是通过servcie字段来区分要存储的内容.
  2. accessGroup字段为空就行,keychain会从所有的访问组中查找service字段匹配的数据.

4 示例代码说明

  1. 示例代码实现了对钥匙链项目的基本的增删改查操作.
  2. 对KeyChain项目的数据进行了简单的封装.
  3. 保存的数据在应用删除也不会丢失,再次安装后,自动恢复.
  4. 在进行了2.5步骤的操作的所有应用之间可以实现钥匙链数据共享.
    Demo的地址为:
    https://github.com/GikkiAres/GaKeyChainManager

5 参考资料

  • Article|Sharing Access to Keychain Items Among a Collection of Apps 文章|多个应用共享钥匙链项目
  • Article|Searching for Keychain Items 文章|搜索钥匙链项目
  • Article|Adding a Password to the Keychain 文章|添加密码到钥匙链

你可能感兴趣的:(iOS | Uuid+KeyChain 让你的设备全球唯一)