雖然目前市面上有一些不錯的加密相冊App,但不是內置廣告,就是對上傳的張數有所限制。本文介紹了一個加密相冊的制作過程,該加密相冊將包括多密碼(輸入不同的密碼即可訪問不同的空間,可掩人耳目)、WiFi傳圖、照片文件加密等功能。目前項目和文章會同時前進,項目的源代碼可以在github上下載。
點擊前往GitHub
本文主要介紹加密相冊的登錄驗證與注冊模塊的實現。注冊時只需要密碼,每個密碼對應一個獨立的存儲空間,登錄時通過Touch ID或密碼驗證。如果有多套密碼,Touch ID會被綁定到一個主密碼上(可更改)。
由於加密相冊只用於本地,當前設計還未考慮密碼找回,因此賬戶只需要密碼這一字段即可,為了統計當前已有賬戶數量,再使用一個id字段,賬戶類SGAccount
設計如下。
@interface SGAccount : NSObject
@property (nonatomic, assign) NSInteger accountId;
@property (nonatomic, copy) NSString *password;
@end
為了進行歸檔存儲,需要實現NSCoding的相關方法,如下。
#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
對於多個賬戶,使用一個賬戶集合類來管理,賬戶集合類管理所有的賬戶,由於登錄驗證時需要查詢密碼對應的賬戶是否存在,為了高效查找,應該使用以密碼為key的Map,也就是NSDictionary來存儲。
除此之外,還需要記錄Touch ID對應的密碼,綜上所述,設計如下。
@interface SGAccountSet : NSObject
@property (nonatomic, strong) NSMutableDictionary *accountMap;
@property (nonatomic, copy) NSString *touchIDPassword;
@end
同理這些屬性也需要在NSCoding的相關方法裡處理,類的實現如下。
#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 *)accountMap {
if (_accountMap == nil) {
_accountMap = @{}.mutableCopy;
}
return _accountMap;
}
@end
對於accountMap的懶加載,可以保證在沒有賬戶數據時拿到的字典不為空。
賬戶管理類對外提供的接口主要是注冊與驗證,為了方便,作為單例使用。
注冊時只需提供密碼即可,而驗證包括兩種情況,其一是通過密碼驗證,第二是通過Touch ID驗證,當驗證成功時直接返回賬戶類。
除此之外,賬戶管理類還有一個屬性currentAccount記錄當前驗證成功的賬戶,以便後續使用,具體設計如下。
@interface SGAccountManager : NSObject
+ (instancetype)sharedManager;
- (void)registerAccountWithPassword:(NSString *)password errorMessage:(NSString * __autoreleasing *)errorMessage;
- (SGAccount *)getAccountByPwd:(NSString *)pwd;
- (SGAccount *)getTouchIDAccount;
/*
* 用於AppDelegate獲取窗口的根控制器
* 沒有注冊過賬戶則進入注冊頁面
* 注冊過用戶則進入登錄驗證頁面
*/
- (UIViewController *)getRootViewController;
@property (nonatomic, strong) SGAccount *currentAccount;
@end
私有接口用於管理類內部的邏輯實現,其中accountSet用於存儲所有用戶數據,accountPath用於存儲賬戶數據保存和加載的路徑。
@interface SGAccountManager ()
@property (nonatomic, strong) SGAccountSet *accountSet;
@property (nonatomic, copy) NSString *accountPath;
@end
賬戶集合accountSet的懶加載
賬戶集合類的初始化包括兩個步驟,首先從硬盤加載數據,如果硬盤上沒有數據,則初始化一個。之所以分解為兩個方法,是因為從硬盤加載數據的方法loadAccountSet會被在其他地方調用,實現如下。
- (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的懶加載
賬戶數據的存儲路徑會在加載和寫入賬戶集合類數據時使用,實現如下。
- (NSString *)accountPath {
if (_accountPath == nil) {
_accountPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"account.agony"];
}
return _accountPath;
}
注冊時傳入密碼,密碼經過加密後,先判斷賬戶集合中是否已經存在此密碼,以防止密碼重復,這是因為密碼與存儲空間一一對應,因此密碼不能重復。如果密碼重復,則通過傳入的字符串指針回傳。
對於第一次注冊的密碼,將會被綁定到Touch ID上,以後使用Touch ID驗證時則相當於輸入此密碼,注冊方法的實現如下。
- (void)registerAccountWithPassword:(NSString *)password errorMessage:(NSString * __autoreleasing *)errorMessage {
NSAssert(password != nil, @"password cannot be nil");
// 對密碼進行MD5+鹽的加密處理
password = [self encryptString:password];
SGAccount *account = self.accountSet.accountMap[password];
// 如果根據要注冊的密碼能取到賬戶,則說明密碼重復,回傳錯誤並返回
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驗證對應的密碼上
self.accountSet.touchIDPassword = password;
}
// 將內存數據同步到硬盤
[self saveAccountSet];
}
加密方法的實現如下。
- (NSString *)encryptString:(NSString *)string {
return [[[[NSString stringWithFormat:@"allowsad12345%@62232",string] MD5] MD5] MD5];
}
MD5方法通過分類的形式添加到NSString上,實現如下。
#import "NSString+MD5.h"
#import
@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
將數據寫入到硬盤的方法實現如下。
- (void)saveAccountSet {
[NSKeyedArchiver archiveRootObject:self.accountSet toFile:self.accountPath];
}
通過密碼驗證的方式,先將密碼加密,再與集合中的密碼比對,找到匹配的則驗證成功,實現如下。
- (SGAccount *)getAccountByPwd:(NSString *)pwd {
pwd = [self encryptString:pwd];
return self.accountSet.accountMap[pwd];
}
通過Touch ID驗證的方式,需要在Touch ID驗證成功後調用,使用Touch ID對應的密碼進行驗證,實現如下。
- (SGAccount *)getTouchIDAccount {
NSString *pwd = self.accountSet.touchIDPassword;
return self.accountSet.accountMap[pwd];
}
如果已經有了賬戶,則返回導航控制器包裹的驗證控制器SGWelcomeViewController
,如果沒有注冊過賬戶,則先初始化一個導航控制器包裹的SGWelcomeViewController
,並且向視圖棧中push一個注冊控制器SGRegisterViewController
,之所以這麼做,是為了保證注冊完成後能夠返回到驗證控制器,並與從驗證頁面進入的注冊保持相同的邏輯,具體實現如下。
- (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;
}
本文主要介紹了與注冊與登錄驗證有關的數據類和管理類的接口與實現過程,在後面的注冊與登錄驗證視圖設計中,只需要使用工具類即可。歡迎關注項目後續,項目的下載地址在本文的開頭可以找到。