开发iOS中的HTTPS

苹果公司在2016的开发者大会上宣布:到2017年,所有的iOS应用都必须使用HTTPS与服务器进行通信。iOS开发者应该都不会对这个决定感到惊讶,因为自iOS9就已经引入了ATS(应用传输安全App Transport Security)特性,该特性对应用的安全传输做出了以下要求:
The protocol Transport Security Layer (TLS) must be at least version 1.2.
Connection ciphers are limited to those that provide forward secrecy (see the list of ciphers below.)
Certificates must use at least an SHA256 fingerprint with either a 2048 bit or greater RSA key, or a 256 bit or greater Elliptic-Curve (ECC) key.
不符合以上条件的任意一项,网络请求将会被中断并返回空值。

既然苹果公司划出了最后期限,HTTPS将成为必然,那么一个iOS开发自学者自然会提出以下问题:

HTTPS的原理和运行机制是什么?
为实现HTTPS通信,服务器端需要做什么?
为实现HTTPS通信,iOS客户端需要做什么?
笔者将以上述问题和学习中遇到的新问题为牵引,逐个进行学习与实践。

HTTPS的原理及运行机制

网络上有很多对于SSL/TLS协议进行讲解的文章,比如SSL/TLS协议运行机制的概述和图解SSL/TLS协议两篇就很不错,推荐阅读。阅读之后能基本理解HTTPS的原理和运行机制,本小节简要归纳自己的理解。
首先,不使用SSL/TLS的HTTP通信,是不加密的通信,这显然不能保证客户的信息安全。在上一篇功课iOS开发中使用keyChain保存用户密码中,辛辛苦苦地将用户名密码能够安全地存储在keyChain中(不越狱的前提下),但是如果用户名密码就这样明文地发送出去,一旦被截获,后果可想而知。当然,我们很自然地就会想到将用户密码字符串进行MD5加密后,再通过网络发送给服务器,比如:

NSString *sourceStr = [NSString stringWithFormat:@”attach=iOS&chartset=utf-8&format=json&partner=google&userid=%@&password=%@”,userid,password];
NSString *signStr = [NSString md5String:sourceStr];
但即便如此也不能保证安全,因为存在碰撞攻击。关于碰撞攻击,可以阅读安全科普:密码学之碰撞攻击一文。

于是,为了实现通信的安全,SSL/TLS协议采用公钥加密法,其运行的基本流程是:

客户端向服务器端索要并验证公钥;
双方协商生成”对话密钥”;
双方采用”对话密钥”进行加密通信。
其中,第1和2步被称为握手阶段。握手阶段的细节这里就不赘述,我们只需要知道,通过握手阶段,客户端和服务器端主要交换了3个信息:
数字证书。该信息是我们进行开发需要关注的!数字证书包含了公钥等信息,一般由服务器发给客户端,接收方通过验证这个证书是不是由信赖的CA签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证;
3个随机数。3个随机数是用于生成对话密钥的,我们不需要关心这细节;
加密通信协议。客户端和服务器端通信需要采取同样的加密通信协议,我们也不需要太关注。
数字证书与公钥基础设施

对于不大接触网络安全的同学,看了上面的原理,可能和我一样也是充满问题,这是因为我们是半路出家,既不知道顶层构架,又不接地气。但是如果对于公钥基础设施(PKI)和数字证书的关系有了一个直观的理解,疑惑就会迎刃而解。

刚才已经了解到HTTPS是基于公钥加密法,而公钥基础设施(PKI)就是一种遵循标准的利用公钥加密技术为电子商务的开展提供一套安全基础平台的技术和规范。完整的PKI系统拥有权威认证机构(CA)、数字证书库、密钥备份及恢复系统、证书作废系统、应用接口(API)等基本构成部分,其中权威认证机构CA将是我们需要打交道的部门。

该系统的逻辑关系可以这样理解:

申请人向CA提交申请材料;
数字证书是由证书认证机构(CA)对证书申请者真实身份验证之后,用CA的根证书对申请人的一些基本信息以及申请人的公钥进行签名(相当于加盖发证书机构的公章)后形成的一个数字文件。CA完成签发证书后,会将证书发布在CA的证书库(目录服务器)中,任何人都可以查询和下载,因此数字证书和公钥一样是公开的。
每个证书持有人都有一对公钥和私钥,这两把密钥可以互为加解密。公钥是公开的,不需要保密,而私钥是由证书持有人自己持有,并且必须妥善保管和注意保密。
简单地说,数字证书就是经过CA认证过的公钥,而私钥一般情况都是由证书持有者在自己本地生成的,由证书持有者自己负责保管。
至此,逻辑链条就和SSL/TLS协议运行机制衔接上了:申请人事先通过向CA申请,已经有了公钥和私钥。客户端向服务器请求,能够得到包含公钥的数字证书。得到公钥后,双方就按照上节中运行的基本流程生成对话密钥,而后开始加密通信。如果对于数字证书和CA之间的关系还不是很清楚,建议阅读数字证书及CA的扫盲介绍一文。

思维比较敏锐的同学也许会问:

我怎么知道数字证书是由CA签发的,而不是第三方伪造的呢?
问得好!简单地说,就是用CA的组织结构和数字证书的签发流程来保证,具体细节在这里不讨论,感兴趣的同学可以阅读iOS安全系列之一:HTTPS一文。
小结:客户端向服务器发出请求得到包含公钥的数字证书,数字证书的真实性是由CA来进行保证。基于公钥,客户端和服务器端能够通过“握手”建立加密的通信。

服务器端如何实现HTTPS

如何实现,肯定得靠自己搭建一个HTTPS服务器啊。根据网络上的资料,可知有两种方式来搭建HTTPS服务器:

一种是创建证书请求,然后到权威机构认证,随之配置到服务器;
一种是自建证书,然后配置给服务器。
第一种方式搭建的HTTPS服务器是最优的。建立网站的话,直接就会被信任。而服务器作为移动端app的服务器时,也不需要为ATS做过多的适配(正是我所需要积累知识的方向)。虽然说权威的机构认证都是需要钱的,但是如今也不乏存在免费的第三方认证机构;

第二种方式搭建的HTTPS服务器,对于网站来说完全不可行,用户打开时直接弹出一个警告提醒,说这是一个不受信任的网站,让用户是否继续,体验很差,而且让用户感觉网站不安全。对于移动端来说,在iOS9出现之前,这个没什么问题,但是在iOS9出来之后,第二种方式是通不过ATS特性,需要在info.plist文件中将App Transport Security Settings中的Allow Arbitrary Loads设置为YES才行。

在本文中,为了快速地验证iOS端与HTTPS服务器能够不需要为ATS做过多的适配,采取了选择一个现成的HTTPS服务器来做验证的方式,学习如何自己搭建的工作,放到下一篇功课中。那么如何才能知道一个HTTPS服务器是符合ATS特性中的要求的呢?使用nscurl命令如下:

nscurl –ats-diagnostics –verbose https://itcyz.cn
命令后接的URL可以使你想要检测的HTTPS服务器地址。接下来,让我们看看知乎能不能通过ATS检测,在终端中输入命令:

nscurl –ats-diagnostics –verbose https://www.itcyz.cn
输出如下:

Starting ATS Diagnostics

Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https://www.itcyz.cn.

A test will “PASS” if URLSession:task:didCompleteWithError: returns a nil error.

Default ATS Secure Connection

ATS Default Connection
ATS Dictionary:
{
}

Result : PASS

=======================================================================

Allowing Arbitrary Loads


Allow All Loads
ATS Dictionary:
{
NSAllowsArbitraryLoads = true;
}

Result : PASS

=======================================================================

Configuring TLS exceptions for www.zhihu.com


TLSv1.2
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.2”;
};
};
}

Result : PASS


TLSv1.1
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.1”;
};
};
}

Result : PASS


TLSv1.0
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.0”;
};
};
}

Result : PASS

=======================================================================

Configuring PFS exceptions for www.zhihu.com


Disabling Perfect Forward Secrecy
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS

=======================================================================

Configuring PFS exceptions and allowing insecure HTTP for www.zhihu.com


Disabling Perfect Forward Secrecy and Allowing Insecure HTTP
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS

=======================================================================

Configuring TLS exceptions with PFS disabled for www.zhihu.com


TLSv1.2 with PFS disabled
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.2”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS


TLSv1.1 with PFS disabled
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.1”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS


TLSv1.0 with PFS disabled
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionMinimumTLSVersion = “TLSv1.0”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS

=======================================================================

Configuring TLS exceptions with PFS disabled and insecure HTTP allowed for www.zhihu.com


TLSv1.2 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionMinimumTLSVersion = “TLSv1.2”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS


TLSv1.1 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionMinimumTLSVersion = “TLSv1.1”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS


TLSv1.0 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
NSExceptionDomains = {
“www.zhihu.com” = {
NSExceptionAllowsInsecureHTTPLoads = true;
NSExceptionMinimumTLSVersion = “TLSv1.0”;
NSExceptionRequiresForwardSecrecy = false;
};
};
}

Result : PASS

=====================================================================
可以看到知乎在所有的测试案例中都是PASS,由此证明知乎通过了ATS检测,给知乎对于用户信息安全的保护点个赞。

然后,进入喜闻乐见的黑百度时间:

nscurl –ats-diagnostics –verbose https://www.baidu.com
然而结果显示,百度同样通过了ATS测试,所以说百度做得也不错。最后被黑到的是新浪微博,感兴趣的知友可以试试,这里就不赘述了。现在我们已经有了百度和知乎这两个HTTPS服务器,接下来就看客户端是否能够成功访问了。

iOS端如何实现HTTPS

基于对HTTPS运行机制的理解,我们知道,在iOS客户端实现与服务器的HTTPS通信,前提条件是你服务器是一个提供了HTTPS的服务器。如果前提得以满足,那么iOS客户端就需要向服务器发出请求索要公钥,而后验证公钥,然后进行握手,左后开始加密通信。那么,具体怎么做呢?难倒这些都需要我自己实现吗?肯定不是的,这种基础性工作,苹果早就做好了,著名的第三方库AFNetworking也早就做好了。本文中主要学习和实践基于AFNetworking的通信。

在上文中提到过,经过CA认证的HTTPS服务器是最好的,在iOS客户端这里基本上不需要做太多ATS适配,现在我们就尝试一下。在上篇功课使用CocoaPods安装AFNetworking并测试中,已经通过设置ATS能够实现HTTP的GET网络请求,这里我们就基于该项目进行修改如下:

首先,删除掉info.plist文件中App Transport Security Settings及其子项Allow Arbitrary Loads,让ATS恢复到默认状态。
其次,修改viewDidLoad方法中的代码,主要将url修改为HTTPS服务器的url:
– (void)viewDidLoad {
[super viewDidLoad];
// 将上次实验的URL注释掉
// NSString *urlString = @”http://api.jirengu.com/weather.php”;
// NSString *urlString = @”http://itunes.apple.com/search?term=metallica”;

// 使用baidu的HTTPS链接
NSString *urlString = @"https://www.baidu.com";
NSURL *url = [NSURL URLWithString:urlString];

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:url.absoluteString parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {

    NSLog(@"results: %@", responseObject);

} failure:^(NSURLSessionDataTask *task, NSError *error) {

    NSLog(@"results: %@", error);

}];

}
运行项目,得到报错如下:

…JSON text did not start with array or object and option to allow fragments not set
通过查询,发现问题是因为AFNetworking默认把请求的相应结果认为是JSON数据,然而实际上我们输入百度的地址,得到的应该是html数据。但是AFNetworking并不知道,它坚信请求的结果就是一个json文本,然后固执地以json的形式去解析,这样显然没办法把一个网页解析成一个字典或者数组,所以产生了上述错误。

解决办法很简答,在代码中添加一句:

manager.responseSerializer = [AFHTTPResponseSerializer serializer]
告诉AFNetworking别把这个网页当成JSON数据来解析!于是修改代码如下:

  • (void)viewDidLoad {
    [super viewDidLoad];
    // 将上次实验的URL注释掉
    // NSString *urlString = @”http://api.jirengu.com/weather.php”;
    // NSString *urlString = @”http://itunes.apple.com/search?term=metallica”;
    // 使用baidu的HTTPS链接
    NSString *urlString = @”https://www.baidu.com”;
    NSURL *url = [NSURL URLWithString:urlString];

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

    manager.responseSerializer = [AFHTTPResponseSerializer serializer]

    [manager GET:url.absoluteString parameters:nil progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {

    NSLog(@"results: %@", responseObject);
    

    } failure:^(NSURLSessionDataTask *task, NSError *error) {

    NSLog(@"results: %@", error);
    

    }];

}
运行项目,得到输出如下:

2016-10-08 20:00:18.052 HTTPS[1916:359510] results: <3c21444f 43545950 45206874 6d6c3e0a 3c68746d 6c3e0a20 2020203c 212d2d53 54415455 53204f4b 2d2d3e0a 20202020 3c686561 643e0a20 20202020 2020203c ...... 6c3e0a>
Message from debugger: Terminated due to signal 15
显然,这次成功地获取到了数据。将HTTPS链接换为知乎的URL,得到的结果类似。

由此可知,对于符合ATS要求的HTTPS服务器,在iOS端不需要对ATS做特殊的适配就能和HTTPS服务器进行通信。而要符合ATS要求,则需要老老实实地创建证书请求,然后到权威机构认证,随之配置到服务器。

iOS自定义转场动画

iOS常用的转场方式包括push,Modal等
本文介绍自定义Modal转场动画来实现展示小菜单功能
效果图如下:

GIF

功能由四个类组成分别是:
ViewController: 控制器
HXPopoverAnimator: 自定义Modal动画管理者
HXJumpViewController: 要弹出的控制器(小菜单)
HXPresentationController:管理弹出的控制器HXJumpViewController

首先是主控制器界面ViewController

控制器和非常简单,设置titleView后监听titleView的点击弹出jumpVc
jumpVc是要Modal出来的控制器
HXPopoverAnimator是自定义用来管理转场动画的类,我会在下面介绍
需要注意的是这句:
jumpVc.modalPresentationStyle = UIModalPresentationCustom;
这句的作用是让新控制器Modal出来之后
新控制器下面的旧控制器依然显示
如果不设置是不显示的
因为我们新Modal出来的控制器是一个菜单,他并不是全屏显示
所以如果不让旧控制器显示,后果将不堪设想….
我们平时正常的Modal展示出的控制器都是占据整个屏幕
所以旧控制器就算不显示也不会有影响
弹出后效果图:
wechatimg12

- (void)setUpTitleView{
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    [btn setTitle:@"菜单" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
    self.navigationItem.titleView = btn;
}

- (void)btnClick{
    //创建弹出的控制器
    HXJumpViewController *jumpVc = [[HXJumpViewController alloc]init];
    //设置modal的样式  默认是UIModalPresentationNone:该控制器后面的控制器不会显示
    jumpVc.modalPresentationStyle = UIModalPresentationCustom;
    //设置弹出View的frame 是自定义的属性
    self.popoverAnimator.presentFrame = CGRectMake(100, 55, 180, 250);
        //transitioningDelegate实现动画 交给popoverAnimator来做
    jumpVc.transitioningDelegate = self.popoverAnimator;
    [self presentViewController:jumpVc animated:YES completion:nil];
}
//懒加载 
//这里的popoverAnimator一定要是强引用
//不然等btnClick 执行结束后 popoverAnimator销毁了,就不会执行popoverAnimator中定义的dismiss代理方法,我就被坑了...
- (HXPopoverAnimator *)popoverAnimator{
    if (!_popoverAnimator) {
        _popoverAnimator = [[HXPopoverAnimator alloc]init];
    }
    return _popoverAnimator;
}

然后是比较简单的HXPresentationController:

Modal的实现原理是:
把将要弹出控制器的View塞进UIPresentationController里面弹出
所以要自定义了Modal
就要自己操作UIPresentationController
UIPresentationController很简单,只做两件事:
1:添加手势View监听点击dismiss自己
2:设置弹出View的Frame
效果图可参考上图
代码如下:

- (void)containerViewWillLayoutSubviews{
    [super containerViewWillLayoutSubviews];
    //设置弹出View的Frame
    self.presentedView.frame = self.presentFrame;
    //添加手势View
    [self.containerView insertSubview:self.maskView atIndex:0];
}

- (void)tapGes{
    [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}

//懒加载背景View
- (UIView *)maskView{
    if (!_maskView) {
        _maskView = [[UIView alloc]init];
        _maskView.backgroundColor = [UIColor colorWithWhite:0.8 alpha:0.2];
        _maskView.frame = self.containerView.bounds;
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapGes)];
        [_maskView addGestureRecognizer:tap];
    }
    return _maskView;
}

最后是HXPopoverAnimator:

HXPopoverAnimator是继承NSObject 遵循UIViewControllerTransitioningDelegate,UIViewControllerAnimatedTransitioning协议的类
用来管理Modal动画
类代码较多,就不在文章里粘了,请移步demo:
https://github.com/huberyhx/HXCustomModal.git
demo中有详细的代码注释