美文网首页
iOS | Uuid+KeyChain 让你的设备全球唯一

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

作者: BinaryBang | 来源:发表于2020-04-04 23:21 被阅读0次

    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);
        }
    }
    

    需要注意以下几点:

    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 参考资料

    相关文章

      网友评论

          本文标题:iOS | Uuid+KeyChain 让你的设备全球唯一

          本文链接:https://www.haomeiwen.com/subject/okrephtx.html