五、iOS开发中通过配置info.plist文件来允许HTTP协议类型的通讯
前面扯了太多,终于提到重点部分了。Apple在iOS9中就已经漏出一些强制HTTPS通讯的端倪,只是给了开发者一些过渡,在iOS10及以后的审核机制中,Apple对于强制HTTPS的推动将会越来越强,如何让自己的应用程序尽快的适配HTTPS相关的标准,是iOS开发者必须面对的任务。
通过前面的分析我们了解,CA机构签发的证书是被默认信任的,这就是说,如果你的公司比较有钱,愿意花钱从CA机构申请一个付费的证书,那么很幸运,你的iOS工程是不需要做任何修改的,这些CA机构签发的证书是默认受信任的,因此你可以直接在程序中进行HTTPS类型的请求,所需要修改的只是将请求url改成https开头。但是另一种情况,无论出于什么原因,你的后台服务用的是自签名的证书,就想我们上面搭建的HTTPS服务一样,如果在不做任何处理的情况下在项目中访问这样的服务,就会出现问题了,原因是我们自己创建的自签名证书是不受信任的,系统默认拒绝了请求,示例如下:
-(void)normalHttps{
NSURLRequest * req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://localhost:8080/users"]];
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
[[session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@,%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding],error);
}] resume];
}
运行工程后可以看到,并没有获取到相关数据,Xcode提示为:
NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)
好了,那么我们先不管HTTPS的问题,如果我们直接对HTTP协议的服务进行请求,会不会有问题呢,将代码修改如下:
-(void)normalHttps{
NSURLRequest * req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:3000/users"]];
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
[[session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@,%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding],error);
}] resume];
}
需要注意:Express在进行项目模板的创建时,会默认帮我们绑定一个3000端口的HTTP服务。
运行工程后,可以发现HTTP协议的请求也无法访问,报错如下:
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
其意思大致是说应用程序传输安全要求强制使用HTTPS类型的服务,但是开发者可以通过配置info.plsit文件来回避这一政策。这就是我们这节的重点,通过文件配置的方式来跳过应用安全传输协议。
在iOS9之后,开发者可以在Info.plist文件中添加如下键:NSAppTransportSecurity。这个键用来配置APP传输安全的相关策略,是字典类型,其中可以设置的键有五个,如下:
NSAllowsArbitraryLoads:布尔值,默认为NO,设置为YES则代表除了NSExceptionDomains中设置的域名外,其他所有请求的协议类型都不受限制,也就是说可以支持HTTP类型的请求,这个键的作用域是全局的,App内所有的请求都受影响,但是如果开发者设置为了YES,在提交审核时需要说明原因。
NSAllowsArbitraryLoadsForMedia:布尔值,默认为NO,设置为YES的话,则应用程序内所有的媒体数据的加载将不受协议类型的限制,同样如果开发者设置为了YES,则在提交审核时需要说明原因。
NSAllowsArbitraryLoadsInWebContent:布尔值,默认为NO。如果设置为YES,则应用程序内所有WebView的请求加载不受协议类型的限制,开发者设置为了YES,则在提交审核时需要说明原因。
NSAllowsLocalNetworking:布尔值,默认为NO,如果设置为YES,则在加载本地资源时不受安全传输协议的限制。
NSExceptionDomains:字典,其主要对某些特殊域名做限制。其中结构可以表示如下:
NSAppTransportSecurity : Dictionary {
NSAllowsArbitraryLoads : Boolean
NSAllowsArbitraryLoadsForMedia : Boolean
NSAllowsArbitraryLoadsInWebContent : Boolean
NSAllowsLocalNetworking : Boolean
//对某些域名做特殊限制
NSExceptionDomains : Dictionary {
<domain-name-string> : Dictionary {
NSIncludesSubdomains : Boolean
NSExceptionAllowsInsecureHTTPLoads : Boolean
NSExceptionMinimumTLSVersion : String
NSExceptionRequiresForwardSecrecy : Boolean // Default value is YES
NSRequiresCertificateTransparency : Boolean
}
}
}
NSIncludesSubdomains:布尔值,这个键的作用是设置此域名下的所有子域名是否采用和父域名相同的配置。
NSExceptionAllowsInsecureHTTPLoads:布尔值,设置是否允许此域名使用自签名的证书进行请求,默认为NO,如果设置为YES,则在提交时需要说明原因。
NSExceptionMinimumTLSVersion:设置所使用的TLS版本。
NSExceptionRequiresForwardSecret:设置为NO,则不允许向前加密方式。
NSRequiresCertificateTransparency:如果设置为YES,则服务端的证书要有有效的透明时间戳。
六、iOS中使用自签名的证书进行HTTPS请求校验
通过Info.plist文件我们是可以绕过安全传输协议的,但是不幸的是,从文档上看,无论开发者通过哪种方式来绕过安全传输协议,Apple都要求开发者在提审时提供合适的理由,这就是说:如果你使用了HTTP协议的请求,没有充足理由的话,你的App有很大的可能被审核拒绝。因此,更加保险的一种方式是将所有的服务都换成HTTPS协议的,如果有CA证书,当然完事大吉,如果没有,我们也可以通过验证自签名证书的方式来适配HTTPS协议。
在进行HTTPS请求时,服务端会先将证书文件返回给客户端,如果客户端的证书信任列表中包含这个证书,则此请求可以正常进行,如果没有,则请求会被拒绝。因此,在iOS中适配自签名证书的HTTPS请求实际上就是将这个自签名的证书安装进客户端的信任列表。iOS中需要使用的证书是der格式的,可以使用如下命令将pem格式的证书转换成der格式的证书:
openssl x509 -inform PEM -in cert.pem -outform DER -out cert.der
将生成的cert文件添加进工程中,修改请求如下:
-(void)normalHttps{
NSURLRequest * req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://localhost:8080/users"]];
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionTask * task = [session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@,%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding],error);
}];
[task resume];
}
除此之外,需要实现一个SURLSessionDelegate的协议方法如下:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
NSLog(@"证书认证");
//先判断证书是否有效
if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust]) {
//证书验证请求
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
/**
* 导入多张CA证书(Certification Authority,支持SSL证书以及自签名的CA)
*/
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"cert" ofType:@"der"];//自签名证书
NSData* caCert = [NSData dataWithContentsOfFile:cerPath];
//可以添加多张证书
NSArray *caArray = @[caCert];
//验证规则
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
NSMutableArray *pinnedCertificates = [NSMutableArray array];
//进行自签名证书的添加
for (NSData *certificateData in caArray) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
SecTrustResultType result = -1;
//通过本地导入的证书来验证服务器的证书是否可信
SecTrustEvaluate(serverTrust, &result);
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
return [[challenge sender] useCredential: credential
forAuthenticationChallenge: challenge];
}
}
如上修改后,再次运行工程,可以看到已经成功请求到了HTTPS自签名证书服务提供的数据: