簡介
雖然目前市面上有一些不錯(cuò)的加密相冊App,但不是內(nèi)置廣告,就是對上傳的張數(shù)有所限制。本文介紹了一個(gè)加密相冊的制作過程,該加密相冊將包括多密碼(輸入不同的密碼即可訪問不同的空間,可掩人耳目)、WiFi傳圖、照片文件加密等功能。目前項(xiàng)目和文章會(huì)同時(shí)前進(jìn),項(xiàng)目的源代碼可以在github上下載。
點(diǎn)擊前往GitHub
概述
本文主要介紹加密相冊的登錄驗(yàn)證與注冊模塊的實(shí)現(xiàn)。注冊時(shí)只需要密碼,每個(gè)密碼對應(yīng)一個(gè)獨(dú)立的存儲(chǔ)空間,登錄時(shí)通過Touch ID或密碼驗(yàn)證。如果有多套密碼,Touch ID會(huì)被綁定到一個(gè)主密碼上(可更改)。
賬戶數(shù)據(jù)存儲(chǔ)設(shè)計(jì)
賬戶類設(shè)計(jì)
由于加密相冊只用于本地,當(dāng)前設(shè)計(jì)還未考慮密碼找回,因此賬戶只需要密碼這一字段即可,為了統(tǒng)計(jì)當(dāng)前已有賬戶數(shù)量,再使用一個(gè)id字段,賬戶類SGAccount設(shè)計(jì)如下。
@interface SGAccount : NSObject <NSSecureCoding>
@property (nonatomic, assign) NSInteger accountId;
@property (nonatomic, copy) NSString *password;
@end
為了進(jìn)行歸檔存儲(chǔ),需要實(shí)現(xiàn)NSCoding的相關(guān)方法,如下。
#import "SGAccount.h"
NSString * const kSGAccountId = @"kSGAccountId";
NSString * const kSGAccountPwd = @"kSGAccountPwd";
@implementation SGAccount
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeInteger:self.accountId forKey:kSGAccountId];
[encoder encodeObject:self.password forKey:kSGAccountPwd];
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.accountId = [decoder decodeIntegerForKey:kSGAccountId];
self.password = [decoder decodeObjectForKey:kSGAccountPwd];
}
return self;
}
+ (BOOL)supportsSecureCoding {
return YES;
}
@end
賬戶集合類設(shè)計(jì)
對于多個(gè)賬戶,使用一個(gè)賬戶集合類來管理,賬戶集合類管理所有的賬戶,由于登錄驗(yàn)證時(shí)需要查詢密碼對應(yīng)的賬戶是否存在,為了高效查找,應(yīng)該使用以密碼為key的Map,也就是NSDictionary來存儲(chǔ)。
除此之外,還需要記錄Touch ID對應(yīng)的密碼,綜上所述,設(shè)計(jì)如下。
@interface SGAccountSet : NSObject <NSSecureCoding>
@property (nonatomic, strong) NSMutableDictionary<NSString *, SGAccount *> *accountMap;
@property (nonatomic, copy) NSString *touchIDPassword;
@end
同理這些屬性也需要在NSCoding的相關(guān)方法里處理,類的實(shí)現(xiàn)如下。
#import "SGAccountSet.h"
NSString * const kSGAccountSetAccountMap = @"kSGAccountSetAccountMap";
NSString * const kSGAccountSetTouchIDPassword = @"kSGAccountSetTouchIDPassword";
@implementation SGAccountSet
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.accountMap = [decoder decodeObjectForKey:kSGAccountSetAccountMap];
self.touchIDPassword = [decoder decodeObjectForKey:kSGAccountSetTouchIDPassword];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.accountMap forKey:kSGAccountSetAccountMap];
[encoder encodeObject:self.touchIDPassword forKey:kSGAccountSetTouchIDPassword];
}
- (NSMutableDictionary<NSString *,SGAccount *> *)accountMap {
if (_accountMap == nil) {
_accountMap = @{}.mutableCopy;
}
return _accountMap;
}
@end
對于accountMap的懶加載,可以保證在沒有賬戶數(shù)據(jù)時(shí)拿到的字典不為空。
賬戶管理類的設(shè)計(jì)
公有接口設(shè)計(jì)
賬戶管理類對外提供的接口主要是注冊與驗(yàn)證,為了方便,作為單例使用。
注冊時(shí)只需提供密碼即可,而驗(yàn)證包括兩種情況,其一是通過密碼驗(yàn)證,第二是通過Touch ID驗(yàn)證,當(dāng)驗(yàn)證成功時(shí)直接返回賬戶類。
除此之外,賬戶管理類還有一個(gè)屬性currentAccount記錄當(dāng)前驗(yàn)證成功的賬戶,以便后續(xù)使用,具體設(shè)計(jì)如下。
@interface SGAccountManager : NSObject
+ (instancetype)sharedManager;
- (void)registerAccountWithPassword:(NSString *)password errorMessage:(NSString * __autoreleasing *)errorMessage;
- (SGAccount *)getAccountByPwd:(NSString *)pwd;
- (SGAccount *)getTouchIDAccount;
/*
* 用于AppDelegate獲取窗口的根控制器
* 沒有注冊過賬戶則進(jìn)入注冊頁面
* 注冊過用戶則進(jìn)入登錄驗(yàn)證頁面
*/
- (UIViewController *)getRootViewController;
@property (nonatomic, strong) SGAccount *currentAccount;
@end
私有接口設(shè)計(jì)
私有接口用于管理類內(nèi)部的邏輯實(shí)現(xiàn),其中accountSet用于存儲(chǔ)所有用戶數(shù)據(jù),accountPath用于存儲(chǔ)賬戶數(shù)據(jù)保存和加載的路徑。
@interface SGAccountManager ()
@property (nonatomic, strong) SGAccountSet *accountSet;
@property (nonatomic, copy) NSString *accountPath;
@end
賬戶集合accountSet的懶加載
賬戶集合類的初始化包括兩個(gè)步驟,首先從硬盤加載數(shù)據(jù),如果硬盤上沒有數(shù)據(jù),則初始化一個(gè)。之所以分解為兩個(gè)方法,是因?yàn)閺挠脖P加載數(shù)據(jù)的方法loadAccountSet會(huì)被在其他地方調(diào)用,實(shí)現(xiàn)如下。
- (SGAccountSet *)accountSet {
if (_accountSet == nil) {
[self loadAccountSet];
}
return _accountSet;
}
- (void)loadAccountSet {
SGAccountSet *set = [NSKeyedUnarchiver unarchiveObjectWithFile:self.accountPath];
if (!set) {
set = [SGAccountSet new];
}
_accountSet = set;
}
賬戶存取路徑accountPath的懶加載
賬戶數(shù)據(jù)的存儲(chǔ)路徑會(huì)在加載和寫入賬戶集合類數(shù)據(jù)時(shí)使用,實(shí)現(xiàn)如下。
- (NSString *)accountPath {
if (_accountPath == nil) {
_accountPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"account.agony"];
}
return _accountPath;
}
注冊的實(shí)現(xiàn)
注冊時(shí)傳入密碼,密碼經(jīng)過加密后,先判斷賬戶集合中是否已經(jīng)存在此密碼,以防止密碼重復(fù),這是因?yàn)槊艽a與存儲(chǔ)空間一一對應(yīng),因此密碼不能重復(fù)。如果密碼重復(fù),則通過傳入的字符串指針回傳。
對于第一次注冊的密碼,將會(huì)被綁定到Touch ID上,以后使用Touch ID驗(yàn)證時(shí)則相當(dāng)于輸入此密碼,注冊方法的實(shí)現(xiàn)如下。
- (void)registerAccountWithPassword:(NSString *)password errorMessage:(NSString * __autoreleasing *)errorMessage {
NSAssert(password != nil, @"password cannot be nil");
// 對密碼進(jìn)行MD5+鹽的加密處理
password = [self encryptString:password];
SGAccount *account = self.accountSet.accountMap[password];
// 如果根據(jù)要注冊的密碼能取到賬戶,則說明密碼重復(fù),回傳錯(cuò)誤并返回
if (account != nil) {
*errorMessage = @"Account Already Exists";
return;
}
account = [SGAccount new];
// 生成賬戶id
NSInteger accountid = self.accountSet.accountMap.allKeys.count + 1;
account.accountId = accountid;
account.password = password;
// 存入到集合中
self.accountSet.accountMap[password] = account;
if (accountid == 1) {
// 如果是第一次注冊,則將其綁定到Touch ID驗(yàn)證對應(yīng)的密碼上
self.accountSet.touchIDPassword = password;
}
// 將內(nèi)存數(shù)據(jù)同步到硬盤
[self saveAccountSet];
}
加密方法的實(shí)現(xiàn)如下。
- (NSString *)encryptString:(NSString *)string {
return [[[[NSString stringWithFormat:@"allowsad12345%@62232",string] MD5] MD5] MD5];
}
MD5方法通過分類的形式添加到NSString上,實(shí)現(xiàn)如下。
#import "NSString+MD5.h"
#import <CommonCrypto/CommonDigest.h>
@implementation NSString (MD5)
- (NSString *)MD5 {
const char *cStr = [self UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *result = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[result appendFormat:@"%02x",digest[i]];
}
return result;
}
@end
將數(shù)據(jù)寫入到硬盤的方法實(shí)現(xiàn)如下。
- (void)saveAccountSet {
[NSKeyedArchiver archiveRootObject:self.accountSet toFile:self.accountPath];
}
登錄驗(yàn)證的實(shí)現(xiàn)
通過密碼驗(yàn)證的方式,先將密碼加密,再與集合中的密碼比對,找到匹配的則驗(yàn)證成功,實(shí)現(xiàn)如下。
- (SGAccount *)getAccountByPwd:(NSString *)pwd {
pwd = [self encryptString:pwd];
return self.accountSet.accountMap[pwd];
}
通過Touch ID驗(yàn)證的方式,需要在Touch ID驗(yàn)證成功后調(diào)用,使用Touch ID對應(yīng)的密碼進(jìn)行驗(yàn)證,實(shí)現(xiàn)如下。
- (SGAccount *)getTouchIDAccount {
NSString *pwd = self.accountSet.touchIDPassword;
return self.accountSet.accountMap[pwd];
}
窗口根控制器選擇的實(shí)現(xiàn)
如果已經(jīng)有了賬戶,則返回導(dǎo)航控制器包裹的驗(yàn)證控制器SGWelcomeViewController,如果沒有注冊過賬戶,則先初始化一個(gè)導(dǎo)航控制器包裹的SGWelcomeViewController,并且向視圖棧中push一個(gè)注冊控制器SGRegisterViewController,之所以這么做,是為了保證注冊完成后能夠返回到驗(yàn)證控制器,并與從驗(yàn)證頁面進(jìn)入的注冊保持相同的邏輯,具體實(shí)現(xiàn)如下。
- (UIViewController *)getRootViewController {
if ([self hasAccount]) {
return [[UINavigationController alloc] initWithRootViewController:[SGWelcomeViewController new]];
}
SGWelcomeViewController *welcomeVc = [SGWelcomeViewController new];
SGRegisterViewController *registerVc = [SGRegisterViewController new];
UINavigationController *nav = [UINavigationController new];
nav.viewControllers = @[welcomeVc, registerVc];
return nav;
}
總結(jié)
本文主要介紹了與注冊與登錄驗(yàn)證有關(guān)的數(shù)據(jù)類和管理類的接口與實(shí)現(xiàn)過程,在后面的注冊與登錄驗(yàn)證視圖設(shè)計(jì)中,只需要使用工具類即可。歡迎關(guān)注項(xiàng)目后續(xù),項(xiàng)目的下載地址在本文的開頭可以找到。