【问题标题】:Saving Email/Password to Keychain in iOS在 iOS 中将电子邮件/密码保存到钥匙串
【发布时间】:2011-03-09 15:20:50
【问题描述】:

我对 iOS 开发非常陌生,如果这是一个新手问题,请原谅我。我的应用程序有一个简单的身份验证机制,它接受用户的电子邮件地址和密码。我还有一个开关,上面写着“记住我”。如果用户打开该开关,我想保留他们的电子邮件/密码,以便将来可以自动填充这些字段。

我已经将它保存到 plist 文件中,但我知道这不是最好的主意,因为密码是未加密的。我找到了一些保存到钥匙串的示例代码,但说实话,我有点迷茫。对于下面的函数,我不知道如何调用它以及如何修改它以保存电子邮件地址。

我猜它会是:saveString(@"passwordgoeshere");

感谢您的帮助!!!

+ (void)saveString:(NSString *)inputString forKey:(NSString *)account {

    NSAssert(account != nil, @"Invalid account");
    NSAssert(inputString != nil, @"Invalid string");

    NSMutableDictionary *query = [NSMutableDictionary dictionary];

    [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
    [query setObject:account forKey:(id)kSecAttrAccount];
    [query setObject:(id)kSecAttrAccessibleWhenUnlocked forKey:(id)kSecAttrAccessible];

    OSStatus error = SecItemCopyMatching((CFDictionaryRef)query, NULL);
    if (error == errSecSuccess) {
        // do update
        NSDictionary *attributesToUpdate = [NSDictionary dictionaryWithObject:[inputString dataUsingEncoding:NSUTF8StringEncoding] 
                                                                      forKey:(id)kSecValueData];

        error = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributesToUpdate);
        NSAssert1(error == errSecSuccess, @"SecItemUpdate failed: %d", error);
    } else if (error == errSecItemNotFound) {
        // do add
        [query setObject:[inputString dataUsingEncoding:NSUTF8StringEncoding] forKey:(id)kSecValueData];

        error = SecItemAdd((CFDictionaryRef)query, NULL);
        NSAssert1(error == errSecSuccess, @"SecItemAdd failed: %d", error);
    } else {
        NSAssert1(NO, @"SecItemCopyMatching failed: %d", error);
    }
}

【问题讨论】:

  • 我修复了 @Anomie 的代码以使用 ARC 并将其放在 Github 上(我链接到这个答案并在两个文件中都提到了你的用户,但如果你想进一步说明,请告诉我)。我还稍微更改了格式并使方法名称更加通用。 github.com/jeremangnr/JNKeychain

标签: ios


【解决方案1】:

我编写了一个简单的包装器,它允许将任何符合 NSCoding 的对象保存到钥匙串中。例如,您可以将您的电子邮件和密码存储在 NSDictionary 中,并使用此类将 NSDictionary 存储到钥匙串中。

SimpleKeychain.h

#import <Foundation/Foundation.h>

@class SimpleKeychainUserPass;

@interface SimpleKeychain : NSObject

+ (void)save:(NSString *)service data:(id)data;
+ (id)load:(NSString *)service;
+ (void)delete:(NSString *)service;

@end

SimpleKeychain.m

#import "SimpleKeychain.h"

@implementation SimpleKeychain

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (id)kSecClassGenericPassword, (id)kSecClass,
            service, (id)kSecAttrService,
            service, (id)kSecAttrAccount,
            (id)kSecAttrAccessibleAfterFirstUnlock, (id)kSecAttrAccessible,
            nil];
}

+ (void)save:(NSString *)service data:(id)data {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((CFDictionaryRef)keychainQuery);
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}

+ (id)load:(NSString *)service {
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    CFDataRef keyData = NULL;
    if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)keyData];
        }
        @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", service, e);
        }
        @finally {}
    }
    if (keyData) CFRelease(keyData);
    return ret;
}

+ (void)delete:(NSString *)service {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((CFDictionaryRef)keychainQuery);
}

@end

【讨论】:

  • @abe:试试看?根据文档,所有相同的功能都可用,但getKeychainQuery: 可能需要在字典中有额外的参数。
  • 此答案需要为 ARC 更新
  • 我讨厌被问到。但是在使用 Simplekeychain 之后,我将如何实现存储用户名、密码的功能呢?我不能使用 [Simplekeychain setObject:Password.text forKey:Password.text];那么有什么替代方案呢?
  • 我已经创建了一个支持设置帐户的修改版本:gist.github.com/btjones/10287581
  • SecItemDelete 永远不应在密码存储功能中使用。要么你想添加一个新的密码条目(然后没有什么可删除的),要么你想更新一个现有的,然后你应该更新它而不是删除它,然后添加一个新的(如系统不会将其识别为更新,它将将该项目视为新项目-该项目的所有访问列表修改和用户自定义都将丢失-它还将从系统帽子将其放回默认钥匙串的任何钥匙串移回)
【解决方案2】:

ARC 就绪代码:

KeychainUserPass.h

#import <Foundation/Foundation.h>

@interface KeychainUserPass : NSObject

+ (void)save:(NSString *)service data:(id)data;
+ (id)load:(NSString *)service;
+ (void)delete:(NSString *)service;

@end

KeychainUserPass.m

#import "KeychainUserPass.h"

@implementation KeychainUserPass

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (__bridge id)kSecClassGenericPassword, (__bridge id)kSecClass,
            service, (__bridge id)kSecAttrService,
            service, (__bridge id)kSecAttrAccount,
            (__bridge id)kSecAttrAccessibleAfterFirstUnlock, (__bridge id)kSecAttrAccessible,
            nil];
}

+ (void)save:(NSString *)service data:(id)data {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(__bridge id)kSecValueData];
    SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);
}

+ (id)load:(NSString *)service {
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
    [keychainQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
    CFDataRef keyData = NULL;
    if (SecItemCopyMatching((__bridge CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
        }
        @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", service, e);
        }
        @finally {}
    }
    if (keyData) CFRelease(keyData);
    return ret;
}

+ (void)delete:(NSString *)service {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
}

@end

【讨论】:

    【解决方案3】:

    我不认为您建议的方法是执行“记住我”功能的好方法。我相信更好的方法是简单地不将该用户从其在服务器上的帐户中注销。在客户端存储一个带有哈希值的 cookie,并在每次服务器调用时发送它。无论如何,您应该始终对每个服务器调用都这样做,而不是发送本地存储的密码。甚至不要将它们存储在局部变量中。

    如果用户想将密码存储在其钥匙串上,那是与“记住我”完全不同且独立的用户任务。恐怕你把这两个用例搞混了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-11-24
      • 2018-09-04
      • 2017-07-29
      • 1970-01-01
      • 2013-05-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多