1、Sign up/in
1.1 用户登录安全原则
- 不能在网络上传输用户隐私数据的明文。
- 不能在本地和服务器上存储用户隐私数据的明文。
1.2 用户登录流程
- 登录成功之后,应该跳转视图控制器到主页。
- 如果用户上次登录成功,启动应用程序时,直接进入主页。
当用户主动注销的时候,返回登录页面。
在实际开发中,关于网络方面的代码执行,通常会有一个单例统一管理。涉及到网络就涉及到多线程的异步,需要控制最大并发数。
1.3 iOS 中加解密
2、明文登录
-
Objective-C
-
GET 登录
// 用户名 NSString *username = self.usernameText.text; // 用户密码明文 NSString *pwd = self.pwdText.text; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat: @"http://192.168.88.200/login/login.php?username=%@&password=%@", username, pwd]]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil || data != nil) { NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; if ([dict[@"userId"] intValue] > 0) { [self saveUserLoginInfo]; } } }] resume];
-
POST 登录
// 用户名 NSString *username = self.usernameText.text; // 用户密码明文 NSString *pwd = self.pwdText.text; NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/login/login.php"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; request.HTTPBody = [[NSString stringWithFormat:@"username=%@&password=%@", username, pwd] dataUsingEncoding:NSUTF8StringEncoding]; [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil || data != nil) { NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; if ([dict[@"userId"] intValue] > 0) { [self saveUserLoginInfo]; } } }] resume];
-
3、Base64 编码登录
Base64 编码具体实现代码见 GitHub 源码 QExtension
-
Objective-C
-
NSString+Base64.m
// 对 ASCII 编码的字符串进行 base64 编码 - (NSString *)base64Encode { NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding]; return [data base64EncodedStringWithOptions:0]; } // 对 base64 编码的字符串进行解码 - (NSString *)base64Decode { NSData *data = [[NSData alloc] initWithBase64EncodedString:self options:0]; return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; }
-
POST 登录
// 用户名 NSString *username = self.usernameText.text; // 经 base64 编码的用户密码 NSString *pwd = [self.pwdText.text base64Encode]; NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/login/login.php"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; request.HTTPBody = [[NSString stringWithFormat:@"username=%@&password=%@", username, pwd] dataUsingEncoding:NSUTF8StringEncoding]; [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil || data != nil) { NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; if ([dict[@"userId"] intValue] > 0) { [self saveUserLoginInfo]; } } }] resume];
-
4、MD5 加密登录
MD5 加密具体实现代码见 GitHub 源码 QExtension
-
Objective-C
-
NSString+Hash.m
#import <CommonCrypto/CommonCrypto.h> // MD5 散列函数 - (NSString *)md5String { const char *str = self.UTF8String; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CC_MD5(str, (CC_LONG)strlen(str), buffer); return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } // HMAC 散列函数 - (NSString *)hmacMD5StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CCHmac(kCCHmacAlgMD5, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } // 时间戳散列函数 - (NSString *)timeMD5StringWithKey:(NSString *)key { NSString *hmacKey = key.md5String; NSString *hmacStr = [self hmacMD5StringWithKey:hmacKey]; NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; fmt.dateFormat = @"yyyy-MM-ddHH:mm"; NSString *dateStr = [fmt stringFromDate:[NSDate date]]; hmacStr = [hmacStr stringByAppendingString:dateStr]; return [hmacStr hmacMD5StringWithKey:hmacKey]; } // 助手方法 - (NSString *)stringFromBytes:(uint8_t *)bytes length:(int)length { NSMutableString *strM = [NSMutableString string]; for (int i = 0; i < length; i++) { [strM appendFormat:@"%02x", bytes[i]]; } return [strM copy]; }
-
直接 md5 加密
// 用户名 NSString *username = self.usernameText.text; // 经 md5 加密的用户密码 NSString *pwd = [self.pwdText.text md5String]; NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/login/login.php"];
-
md5 + 盐 加密
// “盐”,随机添加的字符串,要够长,够复杂 static NSString *salt = @"FUYGIUHIJKJHVBNOIUYUGVHJK@#$%^&))(&*^%W%$^%ukjsbcjcgvbk,jmhnrhjbjknklHDYCHGNKJB"; // 用户名 NSString *username = self.usernameText.text; // 经 md5 加 盐 加密的用户密码 NSString *pwd = [[self.pwdText.text stringByAppendingString:salt] md5String]; NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/login/login.php"];
-
md5 + HMAC 加密
// 用户名 NSString *username = self.usernameText.text; // 经 md5 加 HMAC 加密的用户密码 NSString *pwd = [[self.pwdText.text hmacMD5StringWithKey:@"qianqian"] md5String]; NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/login/login.php"];
-
md5 + HMAC + 时间戳 加密
// 用户名 NSString *username = self.usernameText.text; // 经 md5 加 HMAC 加 时间戳 加密的用户密码 NSString *pwd = [self.pwdText.text timeMD5StringWithKey:@"qianqian"]; NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/login/loginhmac.php"];
-
登录代码段
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; request.HTTPBody = [[NSString stringWithFormat:@"username=%@&password=%@", username, pwd] dataUsingEncoding:NSUTF8StringEncoding]; [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil || data != nil) { NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; if ([dict[@"userId"] intValue] > 0) { [self saveUserLoginInfo]; } } }] resume];
-
5、完整登录
-
Objective-C
-
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 注册通知 /* AppDelegate 中可以不移除 */ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loginSuccess) name:HQUserLoginSuccessedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(logout) name:HQUserLogoutNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(touchIDAuthentication) name:HQTouchIDAuthenticationNotification object:nil]; // 直接用户登录 // 如果成功,进入主页 [[NetworkTools sharedNetworkTools] userLoginWithTouchID:(BOOL)YES Failed:^{ // 如果失败,进入登录页面 [self logout]; }]; return YES; } /// 登录成功 - (void)loginSuccess { // 显示主页面 [self switchStroyboard:@"Home"]; } /// 用户注销 - (void)logout { // 显示登录页面 [self switchStroyboard:@"Login"]; } /// 指纹验证 - (void)touchIDAuthentication { [self switchStroyboard:@"TouchID"]; } /// 切换显示页面 - (void)switchStroyboard:(NSString *)sbName { UIStoryboard *sb = [UIStoryboard storyboardWithName:sbName bundle:nil]; self.window.rootViewController = sb.instantiateInitialViewController; }
-
HomeViewController.m
#import "NetworkTools.h" /// 用户注销 - (IBAction)logout:(UIButton *)sender { [[NetworkTools sharedNetworkTools] userLogout]; }
-
LoginViewController.m
#import "NetworkTools.h" @property (weak, nonatomic) IBOutlet UITextField *usernameText; @property (weak, nonatomic) IBOutlet UITextField *pwdText; @property (weak, nonatomic) IBOutlet UISwitch *savePwdSwitch; - (void)viewDidLoad { [super viewDidLoad]; [self loadUserInfo]; } /// 用户登录 - (IBAction)postLogin { NetworkTools *tools = [NetworkTools sharedNetworkTools]; tools.username = self.usernameText.text; tools.pwd = self.pwdText.text; tools.isSavePwd = self.savePwdSwitch.isOn; [tools userLoginWithTouchID:(BOOL)NO Failed:^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:@"用户名或密码错误" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alertController animated:YES completion:nil]; }]; } /// 加载用户信息 - (void)loadUserInfo { self.usernameText.text = [NetworkTools sharedNetworkTools].username; self.pwdText.text = [NetworkTools sharedNetworkTools].pwd; } /// 键盘回收 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self.view endEditing:YES]; }
-
TouchIDViewController.m
#import "NetworkTools.h" - (void)viewDidLoad { [super viewDidLoad]; [self touchIDAuthentication]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self touchIDAuthentication]; } /// 指纹验证 - (void)touchIDAuthentication { NetworkTools *tools = [NetworkTools sharedNetworkTools]; [tools touchIDAuthenticationFailed:^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:@"指纹验证失败,请重试" preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alertController animated:YES completion:nil]; }]; }
-
NetworkTools.h
extern NSString * const HQUserLoginSuccessedNotification; extern NSString * const HQUserLogoutNotification; extern NSString * const HQTouchIDAuthenticationNotification; @interface NetworkTools : NSObject /// 用户名 @property (nonatomic, copy) NSString *username; /// 用户口令 @property (nonatomic, copy) NSString *pwd; /// 是否记住密码 @property (nonatomic, assign) BOOL isSavePwd; /// 单例方法 + (instancetype)sharedNetworkTools; /// 用户登录 - (void)userLoginWithTouchID:(BOOL)touch Failed:(void (^)())failed; /// 用户注销 - (void)userLogout; /// 用户指纹验证 - (void)touchIDAuthenticationFailed:(void (^)())failed; @end
-
NetworkTools.m
#import "NSString+TimePwd.h" #import "SSKeychain.h" #import <LocalAuthentication/LocalAuthentication.h> // 定义通知字符串常量 NSString * const HQUserLoginSuccessedNotification = @"HMUserLoginSuccessedNotification"; NSString * const HQUserLogoutNotification = @"HMUserLogoutNotification"; NSString * const HQTouchIDAuthenticationNotification = @"HQTouchIDAuthenticationNotification"; @implementation NetworkTools /** 单例创建中,使用 allocWithZone, copyWithZone ... 等等方法,会把所有创建第二个实例可能性全部堵死。 在真正开发中,有的时候,会需要额外创建一个副本。 */ + (instancetype)sharedNetworkTools { static id instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (instancetype)init { self = [super init]; if (self) { // 第一次被实例化的时候,加载用户信息 [self loadUserLoginInfo]; } return self; } /// 用户登录 /** 程序启动直接执行,如果登录成功,进入主界面。如果登录失败,进入登录页面。 */ - (void)userLoginWithTouchID:(BOOL)touch Failed:(void (^)())failed { NSString *username = self.username; // md5 + HMAC + 时间戳 加密 NSString *pwd = [self.pwd timeMD5StringWithKey:@"qianqian"]; NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/login/loginhmac.php"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod = @"POST"; request.HTTPBody = [[NSString stringWithFormat:@"username=%@&password=%@", username, pwd] dataUsingEncoding:NSUTF8StringEncoding]; [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil || data != nil) { NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; dispatch_async(dispatch_get_main_queue(), ^{ if ([dict[@"userId"] intValue] > 0) { [self saveUserLoginInfo]; if (touch == NO || [UIDevice currentDevice].systemVersion.floatValue < 8.0) { // 发送登录成功通知 [[NSNotificationCenter defaultCenter] postNotificationName:HQUserLoginSuccessedNotification object:nil]; } else { // 发送指纹验证通知 [[NSNotificationCenter defaultCenter] postNotificationName:HQTouchIDAuthenticationNotification object:nil]; } } else { if (failed != nil) { // 执行失败回调 failed(); } } }); } }] resume]; } /// 用户注销 - (void)userLogout { [SSKeychain deletePasswordForService:[NSBundle mainBundle].bundleIdentifier account:self.username]; // 发送注销通知 [[NSNotificationCenter defaultCenter] postNotificationName:HQUserLogoutNotification object:nil]; } /// 指纹验证 - (void)touchIDAuthenticationFailed:(void (^)())failed { // 实例化本地身份验证上下文 LAContext *context= [[LAContext alloc] init]; // 判断是否支持指纹识别 if (![context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:NULL]) { // 发送登录成功通知 [[NSNotificationCenter defaultCenter] postNotificationName:HQUserLoginSuccessedNotification object:nil]; return; } [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"请验证已有指纹" reply:^(BOOL success, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ // 输入指纹开始验证,异步执行 if (success) { // 发送登录成功通知 [[NSNotificationCenter defaultCenter] postNotificationName:HQUserLoginSuccessedNotification object:nil]; } else { switch (error.code) { case -1: if (failed != nil) { // 执行失败回调 failed(); } break; case -2: case -3: case -4: case -5: case -6: case -7: // 发送注销通知 [[NSNotificationCenter defaultCenter] postNotificationName:HQUserLogoutNotification object:nil]; break; default: break; } } }); }]; } /// 用户登录信息存储 - (void)saveUserLoginInfo { NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults setObject:self.username forKey:@"userNameKey"]; [userDefaults setBool:self.isSavePwd forKey:@"isSavePwdKey"]; [userDefaults synchronize]; if (self.isSavePwd) { // 将密码保存到钥匙串中 [SSKeychain setPassword:self.pwd forService:[NSBundle mainBundle].bundleIdentifier account:self.username]; } else { self.pwd = nil; [SSKeychain deletePasswordForService:[NSBundle mainBundle].bundleIdentifier account:self.username]; } } - (void)loadUserLoginInfo { NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; self.username = [userDefaults objectForKey:@"userNameKey"]; self.isSavePwd = [userDefaults boolForKey:@"isSavePwdKey"]; // 将密码从钥匙串中取出 self.pwd = [SSKeychain passwordForService:[NSBundle mainBundle].bundleIdentifier account:self.username]; } @end
-
NSString+Hash.m
#import <CommonCrypto/CommonCrypto.h> /// 时间戳散列函数 - (NSString *)timeMD5StringWithKey:(NSString *)key { NSString *hmacKey = key.md5String; NSString *hmacStr = [self hmacMD5StringWithKey:hmacKey]; NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; fmt.dateFormat = @"yyyy-MM-ddHH:mm"; NSString *dateStr = [fmt stringFromDate:[NSDate date]]; hmacStr = [hmacStr stringByAppendingString:dateStr]; return [hmacStr hmacMD5StringWithKey:hmacKey]; } /// HMAC 散列函数 - (NSString *)hmacMD5StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CCHmac(kCCHmacAlgMD5, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } /// 散列函数 - (NSString *)md5String { const char *str = self.UTF8String; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CC_MD5(str, (CC_LONG)strlen(str), buffer); return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } /// 助手方法 - (NSString *)stringFromBytes:(uint8_t *)bytes length:(int)length { NSMutableString *strM = [NSMutableString string]; for (int i = 0; i < length; i++) { [strM appendFormat:@"%02x", bytes[i]]; } return [strM copy]; }
-
6、保存用户信息
-
Objective-C
-
明文保存
// 保存用户信息 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults setObject:self.usernameText.text forKey:@"userNameKey"]; [userDefaults setObject:self.pwdText.text forKey:@"userPwdKey"]; [userDefaults synchronize]; // 加载用户信息 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; self.usernameText.text = [userDefaults objectForKey:@"userNameKey"]; self.pwdText.text = [userDefaults objectForKey:@"userPwdKey"];
-
base64 编码保存
// NSString+Base64.m // 对 ASCII 编码的字符串进行 base64 编码 - (NSString *)base64Encode { NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding]; return [data base64EncodedStringWithOptions:0]; } // 对 base64 编码的字符串进行解码 - (NSString *)base64Decode { NSData *data = [[NSData alloc] initWithBase64EncodedString:self options:0]; return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } // 保存用户信息 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults setObject:self.usernameText.text forKey:@"userNameKey"]; // 对密码进行 base64 编码 [userDefaults setObject:[self.pwdText.text base64Encode] forKey:@"userPwdKey"]; [userDefaults synchronize]; // 加载用户信息 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; self.usernameText.text = [userDefaults objectForKey:@"userNameKey"]; // 对密码进行 base64 解码 self.pwdText.text = [[userDefaults objectForKey:@"userPwdKey"] base64Decode];
-
钥匙串保存
// 添加第三方库文件 SSKeychain // 包含头文件 #import "SSKeychain.h" // 保存用户信息 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults setObject:self.usernameText.text forKey:@"userNameKey"]; [userDefaults synchronize]; // 将密码保存到钥匙串中 [SSKeychain setPassword:self.pwdText.text forService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; // 加载用户信息 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; self.usernameText.text = [userDefaults objectForKey:@"userNameKey"]; // 将密码从钥匙串中取出 self.pwdText.text = [SSKeychain passwordForService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text];
-
7、SSKeychain/SAMKeychain 的使用
GitHub 网址:https://github.com/soffes/SAMKeychain
SSKeychain/SAMKeychain 使用 ARC
-
Objective-C
// 添加第三方库文件 SSKeychain // 包含头文件 #import "SSKeychain.h" // 获取所有的账户信息 // 只有同一个开发者开发的应用程序,才能够互相看到账号 NSArray *allAccounts = [SSKeychain allAccounts]; // 获取指定服务名的账户信息 NSArray *accounts = [SSKeychain accountsForService:[NSBundle mainBundle].bundleIdentifier]; // 将密码保存到钥匙串中 /* + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account; 参数: password :密码明文 serviceName:服务名,可以随便写,建议使用 bundleId,应用程序的唯一标示,每一个上架的应用程序,都有一个唯一的 bundleId account :账户名(用户名) */ // 保存 NSString 格式的密码 [SSKeychain setPassword:self.pwdText.text forService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; NSData *pwdData = [self.pwdText.text dataUsingEncoding:NSUTF8StringEncoding]; // 保存 NSData 格式的密码 [SSKeychain setPasswordData:pwdData forService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; // 将密码从钥匙串中取出 // 获取 NSString 格式的密码 self.pwdText.text = [SSKeychain passwordForService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; // 获取 NSData 格式的密码 NSData *pwdData1 = [SSKeychain passwordDataForService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; // 将密码从钥匙串中删除 [SSKeychain deletePasswordForService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text];