建議先大致看一遍再斟酌用哪種方法
前言
最近在重構App。舊的版本登入模組沒有自動登入功能,體驗極其不好。網上搜尋了很多教程都沒找到完整的,故寫篇文章梳理一下。
- 難點一 稽核問題 如果App首次啟動後,載入的是登入介面,並且必須要登入才能進行其他操作,那麼稽核將會是個問題。本文不涉及這方面的知識(反正這App打包工程師是FAFA,留給他頭疼)。網上涉及這方面的文章挺多的,之前粗略瞭解過,最好提供測試賬號,並且提供測試視訊到美國能直接看的視訊網站。
- 難點二 業務邏輯問題。 有兩種實現方法。 第一種比較低階的。每次App啟動都進入登入介面,如果之前設定了自動登入,就呼叫登入按鈕的方法。實現起來比較簡單。 第二種是仿WX的,也是現在的主流App採用的。如果之前登入成功過並且沒有退出賬號,那麼下次開啟App就進入功能主介面,不進入登入介面。 還有用來記錄登入狀態的flag狀態的改變(最後總結說)。
- 難點三 ?第二種方法中的登入令牌 token token是用來判斷當前使用者的登入狀態!
- 難點四 本地密碼加密(最後發現自動登入可以不需要儲存密碼)
先不考慮加密 用UserDefault儲存
Demo連結 https://github.com/Hsusue/iOS-AutoLogin 該Demo基於第二種方法,包含了介面邏輯、加密演示、token工具包。 第一種差不多且比較簡單,以下會貼出程式碼。
第一種low的方法
- HSUUserDefault.m封裝了UserDefault的方法
+(void)saveUserDefaultObject:(id)object key:(NSString *)key
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:object forKey:key];
[defaults synchronize];
}
+(id)getUserDefaultObject:(NSString *)key
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
id tempObject = [defaults objectForKey:key];
return tempObject;
}
+(void)removeObjectWithKey:(NSString *)key
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults removeObjectForKey:key];
[defaults synchronize];
}
複製程式碼
- AppDelage.m中 ,設定根控制器為登入控制器
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
LoginVC *vc = [[LoginVC alloc] init];
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];
return YES;
}
複製程式碼
- 登入控制器中 根據之前的設定 佈置UI 若自動登入YES , 觸發登入按鈕方法
- (void)viewDidLoad {
self.userNameTextField.text = [HSUUserDefault getUserDefaultObject:kUserName];
BOOL isRemember = [[HSUUserDefault getUserDefaultObject:kRememberPassword] boolValue];
self.rememberSwitch.on = isRemember;
if (isRemember) {
self.psdTextField.text = [HSUUserDefault getUserDefaultObject:kUserPassword];
}
BOOL isAutoLogin = self.autoLoginSwitch.on;
if (isAutoLogin) {
[self loginBtnClick];
}
}
複製程式碼
- 登入成功回撥方法裡 儲存資料 切換程式根控制器
- (void)loginSuccess {
[HSUUserDefault saveUserDefaultObject:self.userNameTextField.text key:kUserName];
BOOL isAutoLogin = self.autoLoginSwitch.on;
if (isAutoLogin) {
[HSUUserDefault saveUserDefaultObject:@(YES) key:kAutoLogin];
[HSUUserDefault saveUserDefaultObject:@(YES) key:kRememberPassword];
[HSUUserDefault saveUserDefaultObject:self.psdTextField.text key:kUserPassword];
} else { // 不自動登入
[HSUUserDefault saveUserDefaultObject:@(NO) key:kAutoLogin];
BOOL isRememberPsd = self.rememberSwitch.on;
if (isRememberPsd) { // 記住密碼
[HSUUserDefault saveUserDefaultObject:@(YES) key:kRememberPassword];
[HSUUserDefault saveUserDefaultObject:self.psdTextField.text key:kUserPassword];
} else {
[HSUUserDefault saveUserDefaultObject:@(NO) key:kRememberPassword];
[HSUUserDefault saveUserDefaultObject:nil key:kUserPassword];
}
}
// 切換AppDelegate的控制器
HSUTabBarController *tabBarController = [[HSUTabBarController alloc] init];
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.tabBarCtl = tabBarController;
appDelegate.window.rootViewController = appDelegate.tabBarCtl;
}
複製程式碼
- 在登出賬號的方法中 自動登入設為NO 切換程式根控制器
[HSUUserDefault saveUserDefaultObject:@(NO) key:kAutoLogin];
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.tabBarCtl = nil;
LoginVC *loginVC = [[LoginVC alloc] init];
appDelegate.window.rootViewController = loginVC;
複製程式碼
第二種方法
- 在appDelegate.m中 根據是否自動登入 設定程式根控制器
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
BOOL isAutoLogin = [[HSUUserDefault getUserDefaultObject:kAutoLogin] boolValue];
if (isAutoLogin) { // 自動登入 進入主介面
self.tabBarCtl = [[HSUTabBarController alloc] init];
self.window.rootViewController = self.tabBarCtl;
} else { // 進入登入介面
LoginVC *vc = [[LoginVC alloc] init];
self.window.rootViewController = vc;
}
[self.window makeKeyAndVisible];
return YES;
}
複製程式碼
先講到這,這樣看第二種方法是種偽自動登入,當然可以在AppDelegate.m中發起登入請求,但是會怪怪的。
// AppDelegate.m
if (isAutoLogin) { // 自動登入 進入主介面
self.tabBarCtl = [[HSUTabBarController alloc] init];
self.window.rootViewController = self.tabBarCtl;
[self.tabBarCtl autoLogin];
}
// HSUTabBarController.m中 實現API代理方法
- (void)loginSuccess {
// 這裡就不需要設定自動登入什麼的了
// 處理返回的資料
}
- (void)loginFailure {// 密碼錯誤什麼的
// 設定自動登入和記住密碼為NO 密碼為nil
// AlertCtl顯示按鈕 程式根控制器置回登入控制器
}
複製程式碼
總結
- 第一種實質上每次都會呼叫登入API,而第二種正規的做法則要根據token來判斷髮起什麼請求。
2018.7.25更新
接下來了解一下token。因為要用到後臺資料,無demo演示。
token是登入令牌,是用來判斷當前使用者的登入狀態!
畫張圖舉個例子 瞭解token的實現。
當使用者從裝置A登入後,伺服器通過某演算法生成一個token,假設為1(實際上是很長的字串),儲存在資料庫中並且返回這個值給A。A收到後,記錄起來,從此發起(有需要此token的網路請求)都要帶上這個token。如果使用者從裝置B登入,那麼伺服器會生成新的token(假設2),(不支援多裝置登入的情況下)舊的token“1”廢棄。如果A這時發起請求,伺服器驗證token,則可能會告訴A“此賬號在別處登入”。
再具體瞭解token。
- token具有時效性。 這個時效性是後臺根據具體需要設定的。 像聊天的APP,時效性可以長達1年(猜測)。 像支付的APP,時效性就應該短點。 過期後,伺服器接收到請求發現該token已失效,就告訴“發起端長時間未操作,請重新登入”。
- token同時間內可以不唯一、擴充 考慮這種情況,某視訊會員賬號最多支援三個端登入。這時A、B、C能同時觀看視訊。如果這三個token還在有效期內,D這時登入,可能無法登入,或者頂掉一個線上裝置(看後臺怎麼處理)。資料庫的狀態應該是存放有效的三個token。 QQ這種手機端、電腦端原理差不多。
- token狀態的改變 在別處登入、賬號密碼修改、過期,都會導致原token失敗。這時候應該提醒使用者,清空儲存的token並退回登入介面。
使用token
步驟一 token的儲存獲取刪除
對比以下兩種方法。 在安全性來說,兩者都能在未越獄手機直接匯出。 在方便性來說,UserDefault簡單點。 cookie好的地方就是能設定過期時間,能本地直接判斷身份資訊是否過期。
- 方法一 最簡單的儲存到NSUserDefault中
// 儲存
[userDefaults setObject:token forKey:@"token"];
[userDefaults synchronize];
// 獲取
[userDefaults objectForKey:@"token"];
// 刪除
[userDefaults removeObjectForKey:@"token"];
複製程式碼
- 方法二 使用cookie cookie —— 儲存在使用者本地終端上的資料。 其實就是字典生成cookie的檔案儲存到App的包裡。 如果App還接有其他WebView自動登入也是用這方法。
蘋果已經幫我們封裝好了,用到兩個類。 NSHTTPCookie:將字典轉成可識別cookie NSHTTPCookieStorage:儲存NSHTTPCookie的物件 cookie要設定(本地的)過期時間,不然App關閉就會清除!
建議封裝一個工具類用。 HSUCookieTool.h
#import <Foundation/Foundation.h>
@interface HSUCookieTool : NSObject
/**
生成cookie
@param name cookie的名字
@param value cookie的值
@param domain 域名
*/
+ (void)saveCookieWithName:(NSString *)name value:(NSString *)value domain:(NSString *)domain;
/**
刪除cookie
@param name cookie的名字
*/
+ (void)deleteCookieWithName:(NSString *)name;
/**
獲取cookie
@param name cookie的名字
@return 對應的cookie,可能為空
*/
+ (NSHTTPCookie *)cookieWithName:(NSString *)name;
@end
複製程式碼
HSUCookieTool.m
#import "HSUCookieTool.h"
@implementation HSUCookieTool
+ (void)saveCookieWithName:(NSString *)name value:(NSString *)value domain:(NSString *)domain{
// 儲存
NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary];
// 給cookie取名
[cookieProperties setObject:name forKey:NSHTTPCookieName];
// 設定值
[cookieProperties setObject:value forKey:NSHTTPCookieValue];
// 存放目錄 通常@"/"
[cookieProperties setObject:@"/" forKey:NSHTTPCookiePath];
// 設定本地過期時間 一年後
// 不設定關掉App就會清空
[cookieProperties setValue:[NSDate dateWithTimeIntervalSinceNow:3600*24*30*12] forKey:NSHTTPCookieExpires];
// 設定域名
[cookieProperties setObject:[NSURL URLWithString:domain].host forKey:NSHTTPCookieDomain];
// 生成cookie
NSHTTPCookie *httpCookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
// 存入倉庫
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:httpCookie];
}
+ (void)deleteCookieWithName:(NSString *)name {
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
NSLog(@"cookie%@", cookie);
if ([cookie.name isEqualToString:name]) {
[cookieJar deleteCookie:cookie];
}
}
}
+ (NSHTTPCookie *)cookieWithName:(NSString *)name {
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
NSLog(@"cookie%@", cookie);
if ([cookie.name isEqualToString:name]) {
return cookie;
}
}
return nil;
}
@end
複製程式碼
注意的是,相同路徑下再次儲存相同cookie名字會替換掉之前的同名cookie。但還是建議先刪除再新增token。
步驟二 請求中攜帶token
API代理方法應該能設定請求頭 在各API中設定請求頭
- (AFHTTPRequestSerializer <AFURLRequestSerialization> *)requestSerializer {
// 獲取本地的token
NSHTTPCookie *cookie = [HSUCookieTool cookieWithName:@"token"];
NSString *token = cookie.value;
// 假設後臺規定@"AuthorisedToken"
AFHTTPRequestSerializer *requestSer = [AFHTTPRequestSerializer serializer];
[requestSer setValue:token forHTTPHeaderField:@"AuthorisedToken"];
return requestSer;
}
複製程式碼
最後來了解本地資訊加密
研究完token發現,本地不需要記住密碼也能實現自動登入。WX也是這樣。介面可以靠賬號和token實現,沒密碼什麼事,畢竟不在本地儲存密碼比任何加密都來得安全。 但如果後臺沒有實現token,又要實現自動登入,就只能每次呼叫登入介面。這就不可避免要用到密碼。
前面說過了NSUserDefaults和NSHTTPCookie,沒越獄的手機能直接匯出資訊,讓人沒安全感。所以加密還是有需要的。
-
方法一 通過Keychain儲存密碼 iOS的keychain服務提供了一種安全的儲存私密資訊(密碼,序列號,證書等)的方式,每個ios程式都有一個獨立的keychain儲存。 但是刪除App後還存在,感覺不太好。 實質就是換個安全的地方把密碼儲存起來。其使用非常容易,且有不少封裝好的工具庫。這裡就不展開了。
-
方法二 使用演算法加密 蘋果自帶的加密演算法有很多,這裡介紹Base64加密。
用NSString+encrypt實現
//給定一個字串,對該字串進行Base64編碼,然後返回編碼後的結果
- (NSString *)base64EncodeString {
//先把字串轉換為二進位制資料
NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
//對二進位制資料進行base64編碼,返回編碼後的字串
return [data base64EncodedStringWithOptions:0];
}
//對base64編碼後的字串進行解碼
- (NSString *)base64DecodeString {
//1.將base64編碼後的字串『解碼』為二進位制資料
NSData *data = [[NSData alloc]initWithBase64EncodedString:self options:0];
//2.把二進位制資料轉換為字串返回
return [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
}
複製程式碼
更安全的做法
和後臺一起規定一個加密演算法,對網路請求的密碼引數進行加密。。到了伺服器後再解密。這樣即使抓包密碼也能不直接暴露出來。這就需要和後臺商量好了。
總結
-
個人認為普通App(安全性要求不高)最優自動登入方法 1.AppDelegate中判斷好載入主功能介面還是登入介面。 2.登入成功後要設定自動登入,用base64加密方法加密密碼和token後儲存起來。 3.處理好任何有關地方的業務邏輯。
-
業務邏輯 1⃣️儲存賬號 2⃣️是否記住密碼 3⃣️儲存密碼 4⃣️是否自動登入 5⃣️儲存token
登入成功必定1⃣️5⃣️。若選擇(記住密碼和自動登入),2⃣️3⃣️4⃣️。 被頂號或登出賬號要❌4⃣️, ❌5⃣️。 密碼錯誤(突然被修改), ❌2⃣️, ❌3⃣️, ❌4⃣️, ❌5⃣️。 token過期, ❌4⃣️, ❌5⃣️。 有token更新的地方記得更改本地token。
疑惑的地方
- 通過更改window.rootViewController 來切換登入控制器和主功能控制器是否不妥?以前做的專案是present出來的。但網上參考的文章是這樣。
- 儲存token用cookie是不是有點小題大做?比起NSUserDefaults,優勢只有一個本地過期屬性。