今天做一个需求,要求获取到设备的IDFA,但是我们都知道,这个值是会变的,会受到用户的影响,所以就想看能不能做一个持久化(对于用户不允许的情况,可以自己生成一个,实现有具体的开源代码),随着软件的卸载和再安装,这个值始终是不变的。
就想起来了keychain--这个系统级的存储。这也就是为什么有些软件在卸载后重新安装后用户名和密码还在的方法之一(因为还有其他方法)。
除了做数据存储,其实它还可以做APP间的数据共享。
因为这部分概念的东西并不多,所以下面将直接上代码,遇到问题顺带着再说;另:mac和iOS不同,这里只说iOS的。
所有的详细资料在这里。
一、Keychain
1、概念
Keychain是一个储存在文件下的简单数据库。通常情况下,app里有一个简单的keychain,可以被所有的app使用。
Keychain有任意数量的钥匙链(item),该钥匙链里包含一组属性。该属性和钥匙链的类型相关。创建日期和label对所有的钥匙链是通用的。其他的都是根据钥匙链的类型不同而不同,比如,generic password
类型包含service和account属性。
钥匙链可以使用kSecAttrSynchronizable
同步属性,被标记为该属性的值都可以被放置在iCloud的钥匙链中,它会被自动同步到相同账号的设备上。
有些钥匙链需要保护起来,比如密码和私人key,都会被加密;对于那些不需要被保护的钥匙链,比如证书,就不会被加密。
在iOS设备上(手机),当屏幕被解锁时,钥匙链的访问权限就会被打开。
2、访问
首先说明其能保存的类型,有5种:
- kSecClassGenericPassword:存储一般密码,比较常用这个
- kSecClassInternetPassword:存储网络密码
- kSecClassCertificate:存储证书
- kSecClassKey:存储私有密钥
- kSecClassIdentity:存储一个包含证书和私有密钥的item
(1)添加
- 方法:
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
attributes有四部分组成:
a、item的类型,必选,key=kSecClass,value就是上面说的5种;
b、item的存储数据;
c、属性,可以用来做一些标记用于查找或者程序之间的数据分享,但是不同的kSecClass有不同的属性;
d、返回数据类型,它的设置影响参数result。
result:对添加的item的引用,如果没有什么需要,可以设为nil - 使用:
- (void)saveBasicInfo {
CFMutableDictionaryRef mutableDicRef = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(mutableDicRef, kSecClass, kSecClassGenericPassword); //类型
CFDateRef dateRef = CFDateCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent());
CFDictionarySetValue(mutableDicRef, kSecAttrCreationDate, dateRef); //创建时间
CFStringRef strRef = CFSTR("save generic password 2");
CFDictionarySetValue(mutableDicRef, kSecAttrDescription, strRef); //描述
CFStringRef commentStrRef = CFSTR("generic password comment 2");
CFDictionarySetValue(mutableDicRef, kSecAttrComment, commentStrRef); //备注、注释
CFNumberRef creatorRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "zhou");
CFDictionarySetValue(mutableDicRef, kSecAttrCreator, creatorRef); //创建者,只能是四个字符长度
CFNumberRef typeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "type");
CFDictionarySetValue(mutableDicRef, kSecAttrType, typeRef); // 类型
CFStringRef labelRef = CFSTR("label 2");
CFDictionarySetValue(mutableDicRef, kSecAttrLabel, labelRef); //标签,用户可以看到
CFDictionarySetValue(mutableDicRef, kSecAttrIsInvisible, kCFBooleanTrue); //是否不可见、是否隐藏(kCFBooleanTrue、kCFBooleanFalse)
CFDictionarySetValue(mutableDicRef, kSecAttrIsNegative, kCFBooleanFalse); //标记是否有密码(kCFBooleanTrue、kCFBooleanFalse)
CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFDictionarySetValue(mutableDicRef, kSecAttrAccount, accountRef); //账户,相同的账户不允许储存两次,否则会报错
CFStringRef serviceRef = CFSTR("service");
CFDictionarySetValue(mutableDicRef, kSecAttrService, serviceRef); //所具有的服务
CFMutableDataRef genericRef = CFDataCreateMutable(kCFAllocatorDefault, 0);
char * generic_char = "personal generic 2";
CFDataAppendBytes(genericRef, (const UInt8 *)generic_char, sizeof("personal generic 2"));
CFDictionarySetValue(mutableDicRef, kSecAttrGeneric, genericRef); //用户自定义
CFDictionarySetValue(mutableDicRef, kSecValueData, genericRef); //保存值
OSStatus status = SecItemAdd(mutableDicRef, nil); //相同的东西只能添加一次,不能重复添加,重复添加会报错
if (status == errSecSuccess) {
NSLog(@"success");
} else {
NSLog(@"%@",@(status));
}
... //神略的是前面创建的变量的释放
}
- 总结:
1、要保证每次添加都是不同账户
2、注意变量内存释放
3、要注意数据类型要对
(2)查找(最好的办法是根据用户名查找)
- 方法:
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
query的组成:
a、item的类型,必选,key=kSecClass,value就是上面说的5种;
b、要查找的属性以及值,这个可以用来筛选掉多余的选项,这个值越详细,最后获取的结果越精确
c、做进一步的限制,比如大小写是否敏感等等
d、返回匹配项的个数,但是注意kSecReturnData
和kSecMatchLimit不能共存,更多的kSecMatchLimit可以搭配kSecReturnAttributes使用 - 使用:
- (OSStatus)keychainMatching {
CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//查找类型
CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
//匹配的属性 越详细,越精准
// CFStringRef strRef = CFSTR("save generic password");
// CFDictionarySetValue(queryDic, kSecAttrDescription, strRef); //描述
// CFStringRef commentStrRef = CFSTR("generic password comment");
// CFDictionarySetValue(queryDic, kSecAttrComment, commentStrRef); //备注、注释
// CFNumberRef creatorRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "zhou");
// CFDictionarySetValue(queryDic, kSecAttrCreator, creatorRef); //创建者,只能是四个字符长度
// CFNumberRef typeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "type");
// CFDictionarySetValue(queryDic, kSecAttrType, typeRef); // 类型
// CFStringRef labelRef = CFSTR("label");
// CFDictionarySetValue(queryDic, kSecAttrLabel, labelRef); //标签,用户可以看到
//查找的参数
// CFDictionarySetValue(queryDic, kSecMatchLimit, kSecMatchLimitAll); //可以控制当key=kSecReturnAttributes时返回值的个数
//返回类型
// CFDictionarySetValue(queryDic, kSecReturnData, kCFBooleanTrue);
CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching(queryDic, &result);
if (status == errSecSuccess) {
NSLog(@"success:%@",result);
if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //类型判断
NSLog(@"Dictionary");
}
} else {
NSLog(@"%@",@(status));
}
...//神略的是前面创建的变量的释放
}
- 总结:
这里主要注意一下返回类型就好了!!!
(3)删除
- 方法:
OSStatus SecItemDelete(CFDictionaryRef query)
query可以直接自己创建也可以是查找后获得的,具体使用看下面的使用。 - 使用:
方法一:
//直接删除
- (void)deleteItemDirect {
//删除CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword); //这个不要丢!!!
CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
if (accountRef) {
CFRelease(accountRef);
}
OSStatus delStatus = SecItemDelete(queryDic);
if (delStatus == errSecSuccess) {
NSLog(@"delete success");
} else {
NSLog(@"%@",@(delStatus));
}
}
方法二:
//先查找,再删除
- (void)deleteItemWithQuery {
CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
if (accountRef) {
CFRelease(accountRef);
}
CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
CFDictionarySetValue(queryDic, kSecReturnRef, kCFBooleanTrue); //这一句话必须要有,否则删除不了
// CFDictionarySetValue(queryDic, kSecReturnPersistentRef, kCFBooleanTrue); //不能用这句,这句是做什么用的呢?
CFTypeRef result = NULL;
OSStatus queryStatus = SecItemCopyMatching(queryDic, &result);
if (queryDic) {
CFRelease(queryDic);
}
if (queryStatus != errSecSuccess) {
NSLog(@"query failed");
return;
}
if (CFGetTypeID(result) == CFDictionaryGetTypeID()) {
OSStatus delStatus = SecItemDelete(result);
if (delStatus == errSecSuccess) {
NSLog(@"delete success");
} else {
NSLog(@"delete failed:%@",@(delStatus));
}
} else if (CFGetTypeID(result) == CFArrayGetTypeID()) {
CFArrayRef arrRef = result;
for (CFIndex i = 0; i < CFArrayGetCount(arrRef); i++) {
OSStatus delStatus = SecItemDelete(CFArrayGetValueAtIndex(arrRef, i));
if (delStatus == errSecSuccess) {
NSLog(@"delete success");
} else {
NSLog(@"delete failed:%@",@(delStatus));
}
}
}
}
- 总结:
1、不要忘记设置item的类型
2、如果是先查找然后再删除,则需要加上kSecReturnRef或者kSecReturnPersistentRef(这个还没搞懂)
(4)、更新
- 方法:
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate)
attributesToUpdate:包含需要去更新的
在使用上,同样有可以直接定义,然后去更新(其实其在内部先查找了);也可以先查找,再把查找到的作为参数去更新。 - 使用:
方法一:
//更新
- (void)updateItem {
CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
if (accountRef) {
CFRelease(accountRef);
}
CFMutableDictionaryRef updateDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFStringRef strRef = CFSTR("save generic password update");
CFDictionarySetValue(updateDic, kSecAttrDescription, strRef); //描述
OSStatus updateStatus = SecItemUpdate(queryDic, updateDic);
if (updateStatus == errSecSuccess) {
NSLog(@"success");
} else {
NSLog(@"update Failed:%@",@(updateStatus));
}
CFRelease(queryDic);
CFRelease(updateDic);
}
方法二:
- (void)updateItemAfterSearch {
CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
if (accountRef) {
CFRelease(accountRef);
}
CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
CFDictionarySetValue(queryDic, kSecReturnRef, kCFBooleanTrue); //这一句话必须要有,否则删除不了
// CFDictionarySetValue(queryDic, kSecReturnPersistentRef, kCFBooleanTrue); //不能用这句,这句是做什么用的呢?
CFTypeRef result = NULL;
OSStatus queryStatus = SecItemCopyMatching(queryDic, &result);
if (queryDic) {
CFRelease(queryDic);
}
if (queryStatus != errSecSuccess) {
NSLog(@"query failed");
return;
}
if (CFGetTypeID(result) != CFDictionaryGetTypeID()) {
return;
}
CFMutableDictionaryRef updateDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFStringRef strRef = CFSTR("save generic password update with query");
CFDictionarySetValue(updateDic, kSecAttrDescription, strRef); //描述
OSStatus updateStatus = SecItemUpdate(result, updateDic);
if (updateStatus == errSecSuccess) {
NSLog(@"success");
} else {
NSLog(@"update Failed:%@",@(updateStatus));
}
CFRelease(updateDic);
}
(5)获取值
- 返回属性kSecReturnAttributes
属性的返回都是以集合形式返回,即要么是数组+字典,或者就只有字典(默认),所以只要确定了返回类型,然后按照key读取就可以了。如下:
- (OSStatus)keychainMatching {
CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//查找类型
CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef); //账户
//查找的参数
CFDictionarySetValue(queryDic, kSecMatchLimit, kSecMatchLimitOne); //可以控制当key=kSecReturnAttributes时返回值的个数
//返回类型
CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching(queryDic, &result);
if (status == errSecSuccess) {
NSLog(@"success:%@",result);
if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //类型判断
NSLog(@"label:%@",CFDictionaryGetValue(result, kSecAttrDescription));
}
} else {
NSLog(@"%@",@(status));
}
if (queryDic) {
CFRelease(queryDic);
}
return status;
}
- 返回值为kSecReturnData
- (OSStatus)keychainMatching {
CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//查找类型
CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef); //账户
//查找的参数
//返回类型
CFDictionarySetValue(queryDic, kSecReturnData, kCFBooleanTrue);
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching(queryDic, &result);
if (status == errSecSuccess) {
NSLog(@"success:%@",result);
if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //类型判断
NSLog(@"label:%@",CFDictionaryGetValue(result, kSecAttrDescription));
} else if (CFGetTypeID(result) == CFDataGetTypeID()) {
const UInt8 * str = CFDataGetBytePtr(result);
NSLog(@"%s",str);
}
} else {
NSLog(@"%@",@(status));
}
if (queryDic) {
CFRelease(queryDic);
}
return status;
}
- 其他(kSecReturnRef和kSecReturnPersistentRef)
这两个我暂时还没搞懂,搞懂了补上!!!