iOS 开发 -- 使用KeyChain保存用户名、密码并实现自动登录

来源:互联网 发布:js生成随机数不要 编辑:程序博客网 时间:2024/06/08 18:18

一、前言

我的话,只是写了个keychain使用的工具类,让我们使用的时候可以直接调用接口,以求方便。
但是关于keychain的一些概念还有一些官方API我都不打算说的,当然你要看下面一些东西的话可能对keychain需要有一个大概的认识。

至少至少你需要知道keychain的这些个API操作都是需要基于一个字典的,我们把它叫做queryDict好了。这个字典,它里面可以有的key和key对应可以设置的value很多都是定好的(官方文档上有,这些个key、value很多,好多我看不太懂,→_←),当然不是所有的key都需要设置。官方文档在描述SecItemCopyMatching方法使用时,关于如何构建一个query字典是这样说的:

You specify attributes defining a search by adding key-value pairs to the query dictionary.A typical query consists of:The class key (Item Class Key Constant) and a class value constant (Item Class Value Constants), which specifies the class of items for which to search.One or more attribute key-value pairs (Attribute Item Keys and Values), which specify the attribute data to be matched.One or more search key-value pairs (Search Keys), which specify values that further refine the search.A return-type key-value pair (Search Results Constants), specifying the type of results you desire.

keychain的操作就是基于这样一个字典:
- 新增一个keychain item是加这样的包括特定键值对的字典(其实从keychain中取出一项的所有信息来看的话,它也就是通过键值对的形式保存了关于这个item的所有信息)
- 删除一个keychian item,也需要你传一个字典,然后所有item中有符合这个字典中所有键值对的item就会被删除
- 改一个keychain item的信息,同样也是需要先用字典的键值对匹配,找到需要改的那一项,然后一个新的键值对替换掉旧的相同key的键值对。
- 查找keychain中的某一个item,就是依据你给的键值对字典,所有键值对都能匹配上的话,这个item就会被查找出来。

具体的一些详情的话,去看看其他博客怎么说的吧,这篇博客好像说得还可以:
http://www.cnblogs.com/Jenaral/p/5663096.html
另外这篇文章讲KeyChain的用法好像也挺好的:
https://useyourloaf.com/blog/simple-iphone-keychain-access/

二、先看一下自己写的KeyChain工具类

在做app记住密码和自动登录的时候,想要试试用keychain,但是在网上找了些别人写的KeyChain三方,感觉没有好用的,所以就自己写了个keychain使用的工具类。
呃,自己的技术好像没多好,写的可能还有挺多bug的,可能内存管理也有问题,大伙如果会改的话就帮我改改呗,嘿嘿。放到gitHub上吧,喜欢可以来颗星吧,话说GitHub怎么用都没太管,哈哈。
GitHub地址: https://github.com/wushaojun315/KeyChainExample

KeyChain上面的东西仔细看的话还是非常多的,我就只是写了一点我要用到的东西,密钥类别也就只涉及到kSecClassGenericPassword这一类。

其实直接看代码也挺清除的,里面注释也写了很多,用起来还挺容易的吧,虽然可能会有些我没有发现的bug!

2.1. ZZZKeyChainHelper类

首先需要说明,我的KeyChain工具类中保存的账户名密码信息分了两种:
- 普通的账户名密码(用于我们通过用户名获取密码)
- 用于自动登录的账户名密码(用于进入应用后获取应用名密码直接自动登录,不通过登录页面输入信息)

两者基础的query字典是不一样的,而且用于自动登录的账户名密码在keychain中只有一个。

普通的账户名密码中基础的query字典是包含的键值对有:
1. kSecClass:kSecClassGenericPassword
这个表示我们保存的是普通的用户名密码。其他的什么证书什么的我们不涉及。
2. kSecAttrAccount:对应的账户名
这个就是保存对应账户的用户名了,而且这个键值对是保证在keychain所有item里面,每个item都与其他item不一样的标记(默认账户名不能相同)
3. kSecAttrService:@”com.companyName.appName”
这个是一个其他标记,对于普通类型的来说,所有的item的这个键值对都是一样的,你们可以随意更改后面的这个字符串值。

用于自动登录的用户名密码的基础query字典包含的键值对有:
1. kSecClass:kSecClassGenericPassword
这个表示我们保存的是普通的用户名密码而不是保存的什么证书信息,跟上面的普通账户类型相同。
2. kSecAttrService:@”autoLoginAccountIdentifierTag”
这个是我为自动登录账户设定的一个标记,跟上面的普通类型不一样。

通过这样两种基础query字典,我们可以保证keychain中每一项的唯一性。
但是这样的话,我们会发现,同一个账户,他可以在普通的保存一份,也可以在自动登录的保存一份。所以为了避免这个问题,我们就在保存到自动登录之前,先将普通的那一份删除即可。(已经在工具类中有实现了,不想要的话去找到对应代码删掉吧!)
我们通过用户名查找对应密码的时候,因为对应一个用户名里面只会有一份信息,所以就不要加kSecAttrService这个key到字典中了,这样就既可以查到普通类型中的,又可以查到自动登录类型中的。

2.2. 普通账户保存暴露的接口

/** 增:将账户信息保存到keychain中    当用户名输入框输入完毕之后,如果在keychain中有对应用户名的账户信息,就获取对应密码填入密码输入框中    (实际上,app应用应该都是用户名框中有所有已经保存好的账户信息,可以直接下拉选择的,这样做更常见吧,但是下拉那个东西我懒得写了,哈哈) @param accountIdentifier 用于保存到keychain中账户信息的账户id(这个是登录信息的唯一标识,也作为keychain里面item的唯一标识,一般可以直接传入登录用户名,如果登录用户名也唯一的话) @param accountPassword 用于保存到keychain中账户信息的账户密码 @return 新增是否成功 */+ (BOOL)addAccountInfoToKeyChainWithAccountIdentifier:(NSString *)accountIdentifier                                      accountPassword:(NSString *)accountPassword;/** 删:根据账户id的唯一性从keychain中删除对应账户的信息 @param accountIdentifier 唯一标识一个keychain item的项,也是一个账户的唯一标识(例如,登录用户名) @return 删除是否成功 */+ (BOOL)deleteAccountInfoFromKeyChainWithAccountIdentifier:(NSString *)accountIdentifier;/** 改:修改对应账户id的keychain项信息(主要修改对应账户的账户密码,一般不会直接调用) @param accountIdentifier 账户的唯一标识符,用于表示我们需要修改哪个账户的信息(例如,可以直接传入用户名) @param newPassword 账户需要更改的的新密码 @return 修改是否成功 */+ (BOOL)updateAccountPasswordForAccount:(NSString *)accountIdentifier                        withNewPassword:(NSString *)newPassword;/** 查:根据账户id,获取保存在keychain中对应的账户密码 @param accountIdentifier 账户标识,表示我们需要获取哪个账户的密码 @return 返回账户密码 */+ (NSString *)getAccountPasswordForAccount:(NSString *)accountIdentifier;

2.3. 自动登录账户类型暴露的接口

/** 增:新增一个autoLoginAccount作为标记的用户名密码信息保存到keychain中,作为进入应用之后自动登录的账户    其他保存在keychain里面的用户名和密码不作为自动登录,而是在手动输入用户名之后,如果keychain中有对应的项,那就获取密码,自动填入密码输入框 @param accountIdentifier 账户信息的账户id @param accountPassword 账户信息的账户密码 @return 新增是否成功 */+ (BOOL)addAutoLoginAccountToKeyChainWithAccountIdentifier:(NSString *)accountIdentifier                                           accountPassword:(NSString *)accountPassword;;/** 删:删除保存在keychain中的用于自动登录的账户信息 @return 删除是否成功 */+ (BOOL)deleteAutoLoginAccountFromKeyChain;/** 改:修改保存在keychain中的用于自动登录的账户信息(一般不直接调用) @param newAccountIdentifier 新的用于自动登录的账户id @param newAccountPassword 新的用于自动登录的账户密码 @return 是否修改成功 */+ (BOOL)updateAutoLoginAccountWithNewAccountIdentifier:(NSString *)newAccountIdentifier                                    newAccountPassword:(NSString *)newAccountPassword;/** 查:获取保存在keyichain中用于自动登录的账户信息 @return 账户信息,包括用户名、密码(对应的key的话,在上面定义好了) */+ (NSDictionary *)getAutoLoginAccountInfo;

2.4. keychain中所有item的操作接口

/** 获取所有的保存在keychain中的账户信息(账户名、账户密码等) @return 包含所有登录账户项的数组 */+ (NSArray *)getAllAccountInfosFromKeychain;/** 清空keychain保存的所有账户信息 @return 清空信息是否成功 */+ (BOOL)clearAllAccountInfosInKeychain;

2.5. 所有接口的使用方法

#import "ViewController.h"#import "ZZZKeyChainHelper.h"@interface ViewController ()@property (weak, nonatomic) IBOutlet UITextField *userNameTextField;@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;@property (weak, nonatomic) IBOutlet UILabel *userNameLabel;@property (weak, nonatomic) IBOutlet UILabel *passwordLabel;@end@implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];    // 一进入应用就查看是否保存有自动登录的账户信息    // 如果有自动登录信息的话,直接用账户信息自动登录,直接跳转到主页    // 如果没有自动登录信息的话,需要先跳转到登录信息录入页面,输入登录信息登录后进入主页    NSDictionary *loginInfoDict = [ZZZKeyChainHelper getAutoLoginAccountInfo];    if (loginInfoDict) {        // 有信息的话可以获取用户名、密码进行登录请求,或者做些其他的事(比如下面就是将保存好的账户信息显示在输入框中)        self.userNameTextField.text = loginInfoDict[kAccountIdentifierAccessKey];        self.passwordTextField.text = loginInfoDict[kAccountPasswordAccessKey];        self.userNameLabel.text = loginInfoDict[kAccountIdentifierAccessKey];        self.passwordLabel.text = loginInfoDict[kAccountPasswordAccessKey];    } else {        // 如果返回了空就要跳到登录页面,进行信息录入操作了。。。    }    // 如果要获取到keychain中所有保存的账户信息的话用下面的代码就好了    // 有的应用(像QQ)登录的界面上,用户名选择可以下拉选择,然后如果保存了密码的那就可以直接密码自动填写上,然后调用登录接口就好了    // 所以,如果需要获取这一系列的账户名等等,可以如下使用    NSArray *accountInfoArray = [ZZZKeyChainHelper getAllAccountInfosFromKeychain];    for (NSDictionary *accountInfoDict in accountInfoArray) {        NSLog(@"******************************************");        NSLog(@"标号:%lu", (unsigned long)[accountInfoArray indexOfObject:accountInfoDict]);        NSLog(@"用户名:%@", accountInfoDict[kAccountIdentifierAccessKey]);        NSLog(@"密码:%@", accountInfoDict[kAccountPasswordAccessKey]);        NSLog(@"******************************************");    }    // 如果要清空所有的keychain保存的账户信息就调用下面注释的代码//    [ZZZKeyChainHelper clearAllAccountInfosInKeychain];}- (void)didReceiveMemoryWarning {    [super didReceiveMemoryWarning];}/** 保存用户名和密码的账户信息 */- (IBAction)saveInfoToKeyChain:(id)sender{    [ZZZKeyChainHelper addAccountInfoToKeyChainWithAccountIdentifier:self.userNameTextField.text                                                     accountPassword:self.passwordTextField.text];}/** 删除对应用户名对应的那条账户信息 */- (IBAction)deleteInfoFromKeyChain:(id)sender{    [ZZZKeyChainHelper deleteAccountInfoFromKeyChainWithAccountIdentifier:self.userNameTextField.text];}/** 输入了对应的用户名之后,获取这个账户对应的密码 每个保存在keychain中的账户可以依据用户名完全标识 */- (IBAction)readInfoFromKeyChain:(id)sender{    // 直接通过用户名拿到密码    NSString *password = [ZZZKeyChainHelper getAccountPasswordForAccount:self.userNameTextField.text];    self.passwordLabel.text = password;}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    [self.view endEditing:YES];}/** 将当前输入的用户名和密码信息保存,作为自动登录的账户(以后打开app就不用到登录信息的录入页面,而是直接登录完成了) */- (IBAction)saveAutoLoginInfo:(id)sender{    [ZZZKeyChainHelper addAutoLoginAccountToKeyChainWithAccountIdentifier:self.userNameTextField.text                                                          accountPassword:self.passwordTextField.text];}/** 删除保存的用于自动登录的信息,删除之后就不会自动登录,而是进入登录界面输入信息再登录了 但是并不影响keychain保存的其他用户名密码信息,在输入用户名后,还是可以从中获取到对应的账户密码 */- (IBAction)deleteAutoLoginInfo:(id)sender{    [ZZZKeyChainHelper deleteAutoLoginAccountFromKeyChain];}/** 读取保存在keychain中的,用于作为进入app直接自动登录的账户信息 */- (IBAction)readAutoLoginInfo:(id)sender{    NSDictionary *accountInfoDict = [ZZZKeyChainHelper getAutoLoginAccountInfo];    // 通过定义在文件中的key值获取返回的用户名和字典    NSString *userName = [accountInfoDict objectForKey:kAccountIdentifierAccessKey];    NSString *password = [accountInfoDict objectForKey:kAccountPasswordAccessKey];    // 显示在界面中    self.userNameLabel.text = userName;    self.passwordLabel.text = password;}

三、下面介绍一下我在应用中的使用

3.1. 自动登录

在进入引用后,首先就先判断是否能够自动登录(也就是查看keychain中是否能够获取到标记为自动登录的账户信息),所以在AppDelegate文件中判断:

如果能获取到自动登录信息,那就使用自动登录信息直接调用登录接口(保存好一些需要用的用户信息啥的),然后就直接进入到应用主页上。

如果没有自动登录的账户信息,那就进入到登录页面(上面的情况,如果登录失败的话,也是得跳到登录页面的吧!)

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];    self.window.backgroundColor = [UIColor whiteColor];    // 先得给他一个rootViewController,网络请求是异步的,着陆之后才能判断是到首页还是到登录页    self.window.rootViewController = [[UIViewController alloc] init];    // 从keychain中获取用于自动登录的账户信息    NSDictionary *accountInfoDict = [ZZZKeyChainHelper getAutoLoginAccountInfo];    if (accountInfoDict) {        // 有用于自动登录的账户信息的话,直接用这个账户信息进行登录        [self loginWithAccountInfo:accountInfoDict[kAccountIdentifierAccessKey]                          password:accountInfoDict[kAccountPasswordAccessKey]                          callback:^(BOOL isSuccessed, NSError *error) {                              // 登录成功,直接进入首页                              if (isSuccessed) {                                  [self launchMainPage];                              } else {                                  // 登录失败,回到登录页面登录去吧                                  [self launchLoginPage];                              }                          }];    } else { // 如果没有自动登录的信息的话,还是回登录页面录入登录信息登录吧        [self launchLoginPage];    }    [self.window makeKeyAndVisible];    return YES;}

3.2. 登录页面

首先,登录页面大致就是下面这个样子的:

登录页面

页面有:
- 用户名和密码的输入框:提供用户名和密码
- 登录按钮:响应登录按钮事件
- 记住密码:修改一个标记(标记当前的这个账户是否需要保存到keychain中)
- 忘记密码:这个就不关我们这个话题的事情了。

首先,我们来看看一些怎样的情况要对账户信息出入keychain有操作:

3.2.1,增:保存一个账户信息进keychain

A:这个操作以登录按钮点击时“记住密码”的标记状态为依据,在登录按钮的响应事件中操作,而不是放在“记住密码”点击的时候操作。
否则的话用户不停地点“记住密码”,一会要求记住一会又不记住,我们做的保存操作就白做了还累成狗(而且这个时候都不一定输入了用户名密码呢!)。

B:另外,我们还默认把当前的这个用户保存到keychain的时候是标记为自动登录账户保存的。
谁让我的界面上没有“自动登录”的选项按钮呢,只能这么默认了。如果你的页面上有“自动登录”状态选择项的话就依据那个来吧。

3.2.2,增:保存一个账户信息进keychain并标记为自动登录账户

如果你看了上面的话,那这个问题我们就不用再说了。

如果你的页面中设计了“自动登录”状态选择项的话另做考虑。

说一下我这个的问题:
如果我不把这两个合并在一起的话,那就需要两种保存都保存一遍了(这样是会成功的因为虽然用户名是相同的,但是是否用作自动登录的标记是不同的,保证了唯一性,这个在KeyChain工具设计时有说到。),两种保存都保存一遍的话,那么在使用KeyChain工具获取所有账户信息的时候,这个账户信息就会出现两次。

但是如果我两个合并在一起,那么账户库中就只有一条对应的账户信息。而且正常类型账户密码获取是可行的(因为这种获取,KeyChain工具实现时是通过用户名的唯一性读取到的,不涉及自动登录标记存在与否),自动登录类型的账户名和账户密码获取也是可行的(因为这种获取只是通过自动登录标记的唯一性)。

3.2.3,删:从keychain中删除一个账户的账户信息(通过账户名)

在登录操作时,如果“记住密码”的标记是未选中的,这种时候可能是说新用户不想记住密码,也有可能是说之前记住过密码的用户,现在突然不想记住了,这种情况我们就需要删除这个用户名对应的账户信息了。(这样的删除是没有问题,因为KeyChain工具在实现时是如果查询到有对应账户项才删除的,如果没有那我们就不管的。之前的新增也是一样没有问题的,新增的情况是如果之前有对应账户名的账户信息,那我们只是更新信息,没有的情况下我们才是新增。)

所以我们通过上面三点先有了登录页面登录按钮的响应事件代码了:

- (IBAction)login:(id)sender {    // 登录信息填写完整后,登录    if (self.userNameTxtField.text && ![self.userNameTxtField.text isEqualToString:@""] && self.passwordTxtField.text && ![self.passwordTxtField.text isEqualToString:@""]) {        // 禁止重复点击        self.loginButton.userInteractionEnabled = NO;        [self.loginButton setTitle:@"正在登录..." forState:UIControlStateNormal];        // 如果此时用户的“记住密码”状态是选中的,那么我们就把当前的账户信息保存到keychain中并作为自动登录账户保存        if (self.passwordRembmbered) {            [ZZZKeyChainHelper addAutoLoginAccountToKeyChainWithAccountIdentifier:self.userNameTxtField.text accountPassword:self.passwordTxtField.text];        } else {            // "记住密码"未选中的话,可能表示用户取消记住,所以需要删除keychain中的对应账户            // 其实就算选中了“记住密码”,但是这个账户已经设置为自动登录类型了,那也是需要删除普通记录的(但是这个已经在加入自动登录的时候工具类做好了)            [ZZZKeyChainHelper deleteAccountInfoFromKeyChainWithAccountIdentifier:self.userNameTxtField.text];        }        [(AppDelegate *)[UIApplication sharedApplication].delegate         loginWithAccountInfo:self.userNameTxtField.text         password:self.passwordTxtField.text         callback:^(BOOL isSuccessed, NSError *error) {             // 登录成功后,跳转到首页             if (isSuccessed) {                 [(AppDelegate *)[UIApplication sharedApplication].delegate launchMainPage];             } else {                 // 登录失败的话,弹错误信息,然后让登录按钮重新能够登录吧                 [self showToastWithMessage:error.localizedDescription];                 // 登录不成功,让登录按钮重新可点击                 self.loginButton.userInteractionEnabled = YES;                 [self.loginButton setTitle:@"登录" forState:UIControlStateNormal];             }         }];    } else {        [self showToastWithMessage:@"登录信息填写不完整"];    }}

3.2.4,删:删除keychain中保存有的自动登录账户信息

这个操作我们在用户点击“退出登录”按钮时进行,因为用户在退出登录之后是要跳回到登录页面的,如果是直接关闭应用了,那下次回来应该也是要跳到登录页面的,所以此时就不应该能够获取到自动登录账户信息,所以我们在点击退出登录的时候删除自动登录账户信息。

如果用户没有点击退出登录而直接关闭应用的话,我们是不会删除自动登录账户信息的,等到下一次用户进入应用也是可以获取到自动登录信息而直接进入到应用主页而不用到登录页面输入登录信息。(反正QQ点击退出当前账号的时候是这样的体验。)

注意:我们在删除自动登录账户的过程中是不是先应该把它存成普通账户信息。
用户点击了退出登录,也许他只是想换一个账户登录,在下次他输入这个账户名的时候,他也许还希望你能自动帮他补全密码呢。所以我这边的代码就是这么写的,哈哈哈!(毕竟,如果还希望我不记住这个密码的话,还可以通过不选中“记住密码”按钮的嘛,那个时候我自然会帮你删除啦!)

所以退出登录的操作代码如下:

- (IBAction)logout:(UIButton *)sender {    // 点击删除按钮之后,我们先把keychain保存的自动登录账户信息保存一份普通账户信息,然后删除自动登录标记的那份    // 获取自动登录账户信息    NSDictionary *autoLoginAccountInfo = [ZZZKeyChainHelper getAutoLoginAccountInfo];    // 保存为普通账户信息    [ZZZKeyChainHelper addAccountInfoToKeyChainWithAccountIdentifier:autoLoginAccountInfo[kAccountIdentifierAccessKey] accountPassword:autoLoginAccountInfo[kAccountPasswordAccessKey]];    // 删除自动登记标记的那份信息    [ZZZKeyChainHelper deleteAutoLoginAccountFromKeyChain];    // 随后删除一些保存的用户信息    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserInfoKey];    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kSelectedOrganKey];    [[NSUserDefaults standardUserDefaults] synchronize];    // 最后回到登录页面    [(AppDelegate *)[UIApplication sharedApplication].delegate launchLoginPage];}

3.2.5,改:修改keychain中的某账户对应的账户密码

3.2.6,改:修改keychain中保存的自动登录账户信息

正如KeyChain工具类中写的一样,我感觉还是比较少会用到update的方法的,都是在新增的方法中调用到,如果新增的过程中通过账户名发现是已经保存过的账户信息,那么就会调用update方法。

如果你们又其他业务需要用到update方法的话,可以自己调用就好了,反正也暴露在了.h文件里面。

3.2.7,查:通过一个账户名从keychain中查找对应的账户密码

这个问题,我这边的实现是输入用户名结束之后,就去自动使用这个用户名查询,如果查到了就自动补全在密码输入框中,查不到就用户自己输入。

注意:反正QQ的话,他们的登录界面的账号输入框,旁边是有一个下拉选择的,点击会直接列出所有的账户名,然后点击一个,然后就会自动补全对应的密码输入框。当然QQ大厂具体用的什么技术实现的,本渣渣就不知道了,→_←。
这种一个下拉获取所有账户列表应该是更好的方式吧,毕竟QQ也这么玩,但是我的应用没有这个需求所以就没做这个了。我们使用KeyChain要做这个的话,需要调用工具类的获取所有账户信息的方法,这个方法会返回一个数组。

下面查看一下我使用查询账户密码接口的代码:

- (void)textFieldDidEndEditing:(UITextField *)textField {    if (textField == self.userNameTxtField) {        NSString *password = [ZZZKeyChainHelper getAccountPasswordForAccount:self.userNameTxtField.text];        if (password) {            self.passwordTxtField.text = password;        }    }}

3.2.8,查:查找keychain中的自动登录标记账户

这个操作应该是在用户打开应用的时候进行的,用户打开应用,首先是要查找keychain里面有没有保存自动登录账户信息,如果有就可以直接=登录完让用户直接看到应用主页,而不用到登录页面录入账户信息登录了。当然,如果没有保存过自动登录账户信息,那就只能先到登录页面登录咯。
对应的代码写在AppDelegate文件中的,前面自动登录有过了。


写得好像比较乱,比较繁杂,你们将就看吧,其实不用看也没什么问题,直接用工具类就好了,代码还是挺容易看的。

阅读全文
0 0
原创粉丝点击