一、GET/POST方法简介&用户安全
1、GET/POST方法简介
在客户端和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST。
> GET - 从指定的资源请求数据。
* GET 请求可被缓存,可以保留在浏览器历史记录中,可被收藏为书签。
* GET 请求从数学角度来讲,GET的结果是 幂等 的
* GET 请求有长度限制,在HTTP协议定义中,没有对GET请求的数据大小限制,不过因为浏览器不同,一般限制在2~8K。
* GET 请求的所有参数包装在URL中,并且服务器的访问日志会记录,不要传递敏感信息。
* 参数格式
>> 在资源路径末尾添加 ? 表示追加参数
>> 每一个变量及值按照 变量名=变量值 方式设定,不能包含空格或中文
>> 多个参数使用 & 连接
>> URL 字符中如果包含中文,需要添加百分号转义。
# 在数学中,一个数多次进行该运算所得的结果和进行一次该运算所得的结果是一样的,那么我们就称该运算是幂等的。比如绝对值运算就是一个例子,在实数集中,有abs(a)=abs(abs(a))。
# GET的结果是 幂等 的,是说对同一个URL请求多次获得的结果都是一样的。
> POST - 向指定的资源提交要被处理的数据
* POST 请求不会被缓存,不会保留在浏览器历史记录中,不能被收藏为书签
* POST 向服务器发送数据,也可以获得服务器处理之后的结果,效率不如GET
* POST 提交数据比较大,大小靠服务器的设定值限制,PHP通常限定2M。
* POST 提交的参数包装成二进制的数据体,格式与 GET 基本一致,只是不包含 ?
* URL中 只有资源路径,但不包含参数,服务器日志不会记录参数,相对更安全。
* 所有设计用户隐私的数据(密码,银行卡号)一定记住使用POST方式传递。
2、浏览器演示GET/POST请求的区别。
3、GET缓存实现
> Request缓存请求头
* If-None-Match : 与响应头的Etag相对应,可以判断本地缓存数据是否发生变化。
> GET方法缓存演练
/*
1、请求是可变的,缓存策略要每次都从服务器加载
2、每次得到响应后,需要记录 etag
3、下次发送请求的同时,将 etag 一起发送给服务器,由服务器比较内容有没有变
*/
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSURL *url = [NSURL URLWithString:@"http://localhost/itcast/images/head1.png"];
// NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
// 传递 etag
if (self.etag.length > 0) {
[request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(@"%@,%zd",response,data.length);
// 类型转换(在oc中,如果将父类转换成子类需要强制转换)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
// 获取并记录 etag,区分大小写
self.etag = httpResponse.allHeaderFields[@"Etag"];
// 判断响应状态码是否是 304 Not Modified
if (httpResponse.statusCode == 304) {
NSLog(@"加载本地数据");
// 如果是,使用本地缓存
// 根据请求获得被缓存的响应
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
data = cacheResponse.data;
}
NSLog(@"etag = %@",self.etag);
self.iconView.image = [UIImage imageWithData:data];
}];
}
> 代码小结
* 请求的缓存策略使用NSURLRequestReloadIgnoringCacheData,忽略本地缓存
* 服务器响应结束后,要记录 Etag,服务器内容和本地缓存对比是否变化的重要依据
* 在发送请求时,设置 If-None-Match,并且传人 Etag
* 连接结束后,要判断响应头的状态码,如果是304,说明本地缓存内容没有变化。
4、NSURLCache--设置缓存
> 在iOS中,可以使用NSURLCache类缓存数据
// 设置网络缓存
// 4M 的内存缓存
// 20M 的磁盘缓存
// diskPath:缓存路径 nil:表示使用系统默认的缓存路径:沙盒/Library/Caches
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:@"pkxing"];
// 设置全局缓存
[NSURLCache setSharedURLCache:cache];
> iOS 5之前:只支持内存缓存。从iOS 5开始:同时支持内存缓存和硬盘缓存
# AFNetworking的作者Mattt说:无数开发者尝试自己做一个简陋而脆弱的系统来实现网络缓存功能,殊不知 NSURLCache 只要两行代码就能搞定且好上100倍。
二、用户登录
1、GET登录
> 代码演示
/**
* GET方式登录
*/
- (void)getLogin{
// 创建url
NSString urlStr = [NSString stringWithFormat:@"http://localhost/login.php?username=%@&password=%@",self.userName,self.password];
// 如果url 字符串中,包含中文或空格等特殊字符,需要添加百分号转义
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
// 创建请求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 请求的默认方法就是 GET
NSLog(@"%@",request.HTTPMethod);
// 发送请求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
NSLog(@"%@,%@",response,dict);
}];
}
> URL编码
* Url编码通常也被称为百分号编码(Url Encoding,also known as percent-encoding),是因为它的编码方式非常简单,使用%百分号加上两位的字符——0123456789ABCDEF——代表一个字节的十六进制形式。
* Url编码默认使用的字符集是ASCII
1、 为什么需要编码?
> url支持26个英文字母、数字和少数几个特殊字符,因此,对于url中包含非标准url的字符时,就需要对其进行编码。
2、 如何编码?
NSString urlStr = [NSString stringWithFormat:@"http://localhost/login.php?username=%@&password=%@",self.userName,self.password];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
> GET请求缓存的位置
* GET请求返回的json数据缓存在Caches/bundleId/Cache.db的sqlite数据库中。
* 通过终端查看:进入到 Caches.db 所在的文件夹,
> 执行命令 sqlite3 Cache.db; 打开数据库
> 执行命令 .tables; 可查看数据库的表
> 执行命令 select * from 表名;可查看对应的表数据,即缓存内容。
2、 POST登录
/**
* POST方式登录
*/
- (void)postLogin{
// 创建url
NSURL *url = [NSURL URLWithString:@"http://localhost/login.php"];
// 创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置请求方法为 POST
request.HTTPMethod = @"POST";
// 设置请求体二进制数据
NSString *bodyStr = [NSString stringWithFormat:@"username=%@&password=%@",self.userName,self.password];
#warning HTTPBody 和 HTTPBodyStream 只需要设置其中一个就行了,如果两个都设置了,前面设置的就无效了。
// request.HTTPBody = [bodyStr dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPBodyStream = [[NSInputStream alloc] initWithData:[bodyStr dataUsingEncoding:NSUTF8StringEncoding]];
// 发送请求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
NSLog(@"%@,%@",response,dict);
}];
}
3、POST和GET请求的对比
> URL对比
* GET
1、login.php 负责登录的脚本,提示:上课使用的是 php,而工作中不一定,可能是.jsp,asp,aspx...取决于后台,后台提供什么,客户端就用什么。
2、‘?’ 表示接参数
3、参数格式:变量名=值
4、‘&’ 多个参数拼接
5、包含中文或空格等特殊字符,需要添加百分号转义
* POST
只有一个 URL,不包含参数
> Request对比
* GET
默认方法就是 GET
* POST
1、将字符串转换成二进制数据,设置HTTPBody或HTTPBodyStream
2、指定 request.HTTPMethod = @"POST"
> Connnection 对比
* 就是将请求发送给服务器,获得二进制数据的响应. GET和POST没有区别。
三、模拟登录
1、搭建界面
2、将数据保存到沙盒--用户名和密码
> 为什么要保存用户名和密码?
# 很多移动应用,在用户第一次登录之后,下次再次使用软件的时候都是会自动登录直接进入到应用的主界面,要实现自动登录,就必须在用户第一次登录成功后保存用户的登录信息。
> 登录信息保存到哪里?
# 保存到应用的沙盒中,偏好设置里面
# 将用户信息以明文的方式保存存在很大的安全隐患。
3、安全原则(重要)
> 不能在网络上传输用户隐私数据的明文。
> 不能在本地存储用户隐私数据的明文。
四、Base64
参考网站:http://zh.wikipedia.org/wiki/Base64
1、简介
> 是网络上使用最广泛的编码系统,能够将任何二进制数据,转换成只有65个字符组成的文本文件
> 由 a~z,A~Z,0~9,+,/,=等65个字符组成
> Base64 编码后的结果能够反算,不够安全。
> Base64 是所有现代加密算法的基础算法。
2、原理
-base64的编码都是按字符串长度,以每3个8bit的字符为一组,
-然后针对每组,首先获取每个字符的ASCII编码,
-然后将ASCII编码转换成8bit的二进制,得到一组3*8=24bit的字节
-然后再将这24bit划分为4个6bit的字节,并在每个6bit的字节前面都填两个高位0,得到4个8bit的字节
-然后将这4个8bit的字节转换成10进制,对照Base64编码表,得到对应编码后的字符。
注:如果被编码字符长度不是3的倍数的时候,则都用0代替,对应的输出字符为=
//参考文章 http://www.cnblogs.com/hongru/archive/2012/01/14/2321397.html
3> 终端命令
# 将 abc.png 进行 base64编码,生成 xxx.txt 文件
$ base64 abc.png -o xxx.txt // -o 表示输出
# 将 xxx.txt 解码生成1.png
$ base64 -D xxx.txt -o 1.png // -D 表示 decoder 解码
# 将字符串 ABC 进行 base64 编码
$ echo -n ABC | base64
# 将字符串 QUJD 解码
$ echo -n QUJD | base64 -D
五、Base64代码实现
#pragma mark - Base64
// 编码:A => QQ==
-(NSString *)base64Encode:(NSString *)string{
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
return [data base64EncodedStringWithOptions:0];
}
// 解码:QQ== => A
- (NSString *)base64Decode:(NSString *)string {
NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:0];
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
1、修改代码
> 保存密码不能使用明文,使用 base64进行加密。
> 提交数据到服务器不能使用明文,使用 base64进行加密。
六、MD5加密实现
1、什么是MD5?
* Message Digest Algorithm MD5(中文名为消息摘要算法第五版)是计算机安全领域广泛使用的一种散列函数(也叫Hash函数),用以提供消息的完整性保护。
2、MD5算法具有以下特点
> 压缩性:任意长度的数据,算出的MD5值长度都是固定的。相同的字符串,每次MD5后的结果是固定的。
> 容易计算:从原数据计算出MD5值很容易。
> 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
> 弱抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。
> 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
3、MD5的作用?
> 一致性验证
我们都知道,地球上任何人都有自己独一无二的指纹,这常常成为司法机关鉴别罪犯身份最值得信赖的方法。与之类似,通过MD5就可以为任何文件(不管其大小、格式、数量)产生一个独一无二的"数字指纹",如果任何人对文件做了任何改动,其MD5值也就是对应的"数字指纹"都会发生变化。
具体来说文件的MD5值就像是这个文件的“数字指纹”。每个文件的MD5值是不同的,如果任何人对文件做了任何改动,其MD5值也就是对应的“数字指纹”就会发生变化。比如下载服务器针对一个文件预先提供一个MD5值,用户下载完该文件后,用这个算法重新计算下载文件的MD5值,通过比较这两个值是否相同,就能判断下载的文件是否出错,或者说下载的文件是否被篡改了。
利用MD5算法来进行文件校验的方案被大量应用到软件下载站、论坛数据库、系统文件安全等方面。
> 数字签名
MD5的典型应用是对一段Message(字节串)产生fingerprint(指纹)以防止被“篡改”。
举个例子:
你将一段话写在一个叫readme.txt文件中,并对这个readme.txt产生一个MD5的值并记录在案,然后
你可以传播这个文件给别人,别人如果修改了文件中的任何内容,你对这个文件重新计算MD5时就会发现(
两个MD5值不相同)。如果再有一个第三方的认证机构,用MD5还可以防止文件作者的“抵赖”,这就是所谓
的数字签名应用。
> 安全访问认证
典型案例:加密用户登录密码。
当用户登录的时候,系统把用户输入的密码进行MD5 Hash运算,然后再去和保存在文件系统中的MD5值
进行比较,进而确定输入的密码是否正确。通过这样的步骤,系统在并不知道用户密码的明码的情况下就
可以确定用户登录系统的合法性。这可以避免用户的密码被具有系统管理员权限的用户知道。
4、MD5加密实现
/*
对密码进行 MD5 加密 - 不安全
NSString *password = self.passField.text.md5String;
NSString *password = self.passField.text.md5String.md5String;
*/
> 如何使MD5加密更安全?
1、加盐、现在用的比较少,前两年用得比较多。
// 准备盐
static NSString *salt = @"fadsfdbvcxweioa4321asfFAFA321DSFASDF%$%$^$^$$#@23123124{}{4";
NSString *password = [self.passField.text stringByAppendingString:salt].md5String;
注意点:‘盐’在现实生活中是佐料,就是给密码加点料,salt要够咸(复杂点的字符串)。
2、用HMac。 HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
下面代码 md5的过程:使用密钥itheima对密码加密,加密后做md5,得到32位字符串,再次使用 itheima 加密,再md5。
HMAC现在使用的比较广泛,安全级别更高, 破解难度高。
但还是有风险:每次结果一致,有可能被暴力破解。
要想做到的安全级别更更高,现在密码学要求:同样的算法,同样的密码明文,每次的结果不一样。
NSString *password = [self.passField.text hmacMD5StringWithKey:@"itheima"];
5、使用时间戳,目前使用非常广泛
/**
* 生成带时间戳的密码
*/
- (NSString *)timePassword{
// 1.设置密钥 key
NSString *key = @"itheima".md5String;
// 2.对密钥key对密码进行HMac
NSString *pwd = [self.passField.text hmacMD5StringWithKey:key];
NSLog(@"key = %@",key);
// 3.获得当前的系统时间
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 指定时区,真机通常需要指定时区
fmt.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"zh"];
// 设置时间格式
fmt.dateFormat = @"yyyy-MM-dd HH:mm";
// 格式化当前时间
NSString *dateStr = [fmt stringFromDate:[NSDate date]];
// 4.用密码 + 时间 生成 密码
pwd = [pwd stringByAppendingString:dateStr];
// 5.返回 hmac 结果
return [pwd hmacMD5StringWithKey:key];
}
NSString *password = [self timePassword];
#MD5在线查询网站://http://www.cmd5.com/
6、获得服务器时间,生成带时间戳的密码
- (NSString *)timePassword{
// 1.设置密钥 key
NSString *key = @"itheima".md5String;
// 2.对密钥key对密码进行HMac
NSString *pwd = [self.passField.text hmacMD5StringWithKey:key];
// 3.获得当前服务器的系统时间
NSURL *url = [NSURL URLWithString:@"http://localhost/hmackey.php"];
// 使用同步获取时间(注意:这里要使用同步,确定先获得服务器的时间,后面的代码才能执行)
NSData *timeData = [NSData dataWithContentsOfURL:url];
// 反序列化取出时间
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:timeData options:0 error:NULL];
NSString *dateStr = dict[@"key"];
// 4.用密码 + 时间 生成 密码
pwd = [pwd stringByAppendingString:dateStr];
// 5.返回 hmac 结果
return [pwd hmacMD5StringWithKey:key];
}
> 为什么要获得服务器时间来对密码进行hmac?
# 有些人是走在时间的前面的,他手机上的时间设置会比真实的时间的快5分钟。如果是这样的,就会导致客户端获得的系统时间和服务器获得的系统时间相差几分钟。那就会导致 hmac 的结果不一致,无法登录。
七、钥匙串访问
#使用MD5加密本地密码有个问题:
MD5是不可逆的,本地使用MD5加密密码保存到偏好设置的时候,无法再读取的时候载解析成
原来的文字。而使用 Base64加密又太过简单了,容易破解。这时候就要使用钥匙串了。
1、钥匙串
> 钥匙串访问,使用 AES 256加密算法,能够保证用户密码的安全。
> 钥匙串访问SDK,是苹果ios7.0.3 版本以后发布的
> 钥匙串访问的接口是纯C语言的,但是,网络上有个哥们把它封装成 OC的,使用相当简单!
> 钥匙串访问的密码保存在哪里?
* 只有苹果知道,是为了进一步保障用户的密码安全。
> 钥匙串访问的第三方框架,是对 C 框架的封装,可以不用看源代码
2、框架地址:https://github.com/samsoffes/sskeychain
3、代码实现
// 保存用户登录信息
- (void)saveUserInfo{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setValue:self.nameField.text forKey:HMUsernameKey];
// 保存到密码到钥匙串
/**
参数:
1.密码‘明文’,加密工作苹果做了,使用的是 AES 256 算法
2.服务名,可以随便写,建议使用bundleId
3.帐号,用户名。因为钥匙串访问中,可以保存很多帐号,很多app的密码
*/
NSString *bundleId = [NSBundle mainBundle].bundleIdentifier;
[SSKeychain setPassword:self.passField.text forService:bundleId account:self.nameField.text];
}
// 加载用户登录信息
- (void)loadUserInfo{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
self.nameField.text = [defaults valueForKey:HMUsernameKey];
// 从钥匙串中获得密码
NSString *bundleId = [NSBundle mainBundle].bundleIdentifier;
self.passField.text = [SSKeychain passwordForService:bundleId account:self.nameField.text];
}
八、登录代码抽取
1、代码重构
> 应用程序开发中,通常会建立一个网络请求管理的单例,管理所有的网络请求,这样可以很好的控制并发数。
> 将用户登录部分代码抽取到单例管理,简化控制器的代码。
* 谁调用登录方法最合适? AppDelegate
* 复制 ‘加载用户信息方法’ 到单例对象中
* 在 init 方法中加载用户信息
> 登录成功,就直接显示主界面。
2、登录界面切换
> 如果监听用户登录成功?
* 代理/通知/block/KVO(监听对象属性变化)
代理是一对一的,在单例中不要使用代理。
通知是多对多的,这种情况选择通知。
3、注销切换界面