美文网首页iOSiOS开发iOS学习
iOS使用自签名证书实现HTTPS请求

iOS使用自签名证书实现HTTPS请求

作者: 浪投王 | 来源:发表于2016-09-27 17:12 被阅读11761次

由于苹果规定2017年1月1日以后,所有APP都要使用HTTPS进行网络请求,否则无法上架,因此研究了一下在iOS中使用HTTPS请求的实现。相信大家对HTTPS都或多或少有些了解,这里我就不再介绍了,主要功能就是将传输的报文进行加密,提高安全性。

1、证书准备

证书分为两种,一种是花钱向认证的机构购买的证书,服务端如果使用的是这类证书的话,那一般客户端不需要做什么,用HTTPS进行请求就行了,苹果内置了那些受信任的根证书的。另一种是自己制作的证书,使用这类证书的话是不受信任的(当然也不用花钱买),因此需要我们在代码中将该证书设置为信任证书。

我这边使用的是xca来制作了根证书,制作流程请参考http://www.2cto.com/Article/201411/347512.html,由于xca无法导出.jsk的后缀,因此我们只要制作完根证书后以.p12的格式导出就行了,之后的证书制作由命令行来完成。自制一个批处理文件,添加如下命令:

set ip=%1%
md %ip%
keytool -importkeystore -srckeystore ca.p12 -srcstoretype PKCS12 -srcstorepass 123456 -destkeystore ca.jks -deststoretype JKS -deststorepass 123456
keytool -genkeypair -alias server-%ip% -keyalg RSA -keystore ca.jks -storepass 123456 -keypass 123456 -validity 3650 -dname "CN=%ip%, OU=ly, O=hik, L=hz, ST=zj, C=cn"
keytool -certreq -alias server-%ip% -storepass 123456 -file %ip%\server-%ip%.certreq -keystore ca.jks
keytool -gencert -alias ca -storepass 123456 -infile %ip%\server-%ip%.certreq -outfile %ip%\server-%ip%.cer -validity 3650 -keystore ca.jks 
keytool -importcert -trustcacerts -storepass 123456 -alias server-%ip% -file %ip%\server-%ip%.cer -keystore ca.jks
keytool -delete -keystore ca.jks -alias ca -storepass 123456

将上面加粗的ca.p12改成你导出的.p12文件的名称,123456改为你创建证书的密码。

然后在文件夹空白处按住ctrl+shift点击右键,选择在此处打开命令窗口,在命令窗口中输入“start.bat ip/域名”来执行批处理文件,其中start.bat是添加了上述命令的批处理文件,ip/域名即你服务器的ip或者域名。执行成功后会生成一个.jks文件和一个以你的ip或域名命名的文件夹,文件夹中有一个.cer的证书,这边的.jks文件将在服务端使用.cer文件将在客户端使用,到这里证书的准备工作就完成了。

2、服务端配置

由于我不做服务端好多年,只会使用Tomcat,所以这边只讲下Tomcat的配置方法,使用其他服务器的同学请自行查找设置方法。

打开tomcat/conf目录下的server.xml文件将HTTPS的配置打开,并进行如下配置:

<Connector URIEncoding="UTF-8" protocol="org.apache.coyote.http11.Http11NioProtocol" port="8443" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" sslProtocol="TLSv1.2" sslEnabledProtocols="TLSv1.2" keystoreFile="${catalina.base}/ca/ca.jks" keystorePass="123456" clientAuth="false" SSLVerifyClient="off" netZone="你的ip或域名"/>

keystoreFile是你.jks文件放置的目录,keystorePass是你制作证书时设置的密码,netZone填写你的ip或域名。注意苹果要求协议要TLSv1.2以上

3、iOS端配置

首先把前面生成的.cer文件添加到项目中,注意在添加的时候选择要添加的targets。

1.使用NSURLSession进行请求

代码如下:

NSString *urlString = @"https://xxxxxxx";
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0f];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
[task resume];

需要实现NSURLSessionDataDelegate中的URLSession:didReceiveChallenge:completionHandler:方法来进行证书的校验,代码如下:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    NSLog(@"证书认证");
    if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust]) {
        do
        {
            SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
            NSCAssert(serverTrust != nil, @"serverTrust is nil");
            if(nil == serverTrust)
                break; /* failed */
            /**
             *  导入多张CA证书(Certification Authority,支持SSL证书以及自签名的CA),请替换掉你的证书名称
             */
            NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自签名证书
            NSData* caCert = [NSData dataWithContentsOfFile:cerPath];

            NSCAssert(caCert != nil, @"caCert is nil");
            if(nil == caCert)
                break; /* failed */
           
            SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
            NSCAssert(caRef != nil, @"caRef is nil");
            if(nil == caRef)
                break; /* failed */
           
            //可以添加多张证书
            NSArray *caArray = @[(__bridge id)(caRef)];
           
            NSCAssert(caArray != nil, @"caArray is nil");
            if(nil == caArray)
                break; /* failed */
           
            //将读取的证书设置为服务端帧数的根证书
            OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
            NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
            if(!(errSecSuccess == status))
                break; /* failed */
           
            SecTrustResultType result = -1;
            //通过本地导入的证书来验证服务器的证书是否可信
            status = SecTrustEvaluate(serverTrust, &result);
            if(!(errSecSuccess == status))
                break; /* failed */
            NSLog(@"stutas:%d",(int)status);
            NSLog(@"Result: %d", result);
           
            BOOL allowConnect = (result == kSecTrustResultUnspecified) || (result == kSecTrustResultProceed);
            if (allowConnect) {
                NSLog(@"success");
            }else {
                NSLog(@"error");
            }

            /* kSecTrustResultUnspecified and kSecTrustResultProceed are success */
            if(! allowConnect)
            {
                break; /* failed */
            }
           
#if 0
            /* Treat kSecTrustResultConfirm and kSecTrustResultRecoverableTrustFailure as success */
            /*   since the user will likely tap-through to see the dancing bunnies */
            if(result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure || result == kSecTrustResultOtherError)
                break; /* failed to trust cert (good in this case) */
#endif
           
            // The only good exit point
            NSLog(@"信任该证书");
           
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
            return [[challenge sender] useCredential: credential
                          forAuthenticationChallenge: challenge];
           
        }
        while(0);
    }
   
    // Bad dog
    NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge,credential);
    return [[challenge sender] cancelAuthenticationChallenge: challenge];
}

此时即可成功请求到服务端。

注:调用SecTrustSetAnchorCertificates设置可信任证书列表后就只会在设置的列表中进行验证,会屏蔽掉系统原本的信任列表,要使系统的继续起作用只要调用SecTrustSetAnchorCertificates方法,第二个参数设置成NO即可。

2.使用AFNetworking进行请求

AFNetworking首先需要配置AFSecurityPolicy类,AFSecurityPolicy类封装了证书校验的过程。

/**
 AFSecurityPolicy分三种验证模式:
 AFSSLPinningModeNone:只是验证证书是否在信任列表中
 AFSSLPinningModeCertificate:该模式会验证证书是否在信任列表中,然后再对比服务端证书和客户端证书是否一致
 AFSSLPinningModePublicKey:只验证服务端证书与客户端证书的公钥是否一致
*/

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    securityPolicy.allowInvalidCertificates = YES;//是否允许使用自签名证书
    securityPolicy.validatesDomainName = NO;//是否需要验证域名,默认YES

    AFHTTPSessionManager *_manager = [AFHTTPSessionManager manager];
    _manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    _manager.securityPolicy = securityPolicy;
    //设置超时
    [_manager.requestSerializer willChangeValueForKey:@"timeoutinterval"];
    _manager.requestSerializer.timeoutInterval = 20.f;
    [_manager.requestSerializer didChangeValueForKey:@"timeoutinterval"];
    _manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringCacheData;
    _manager.responseSerializer.acceptableContentTypes  = [NSSet setWithObjects:@"application/xml",@"text/xml",@"text/plain",@"application/json",nil];
 
    __weak typeof(self) weakSelf = self;
    [_manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *_credential) {
       
        SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
        /**
         *  导入多张CA证书
         */
        NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自签名证书
        NSData* caCert = [NSData dataWithContentsOfFile:cerPath];
        NSArray *cerArray = @[caCert];
        weakSelf.manager.securityPolicy.pinnedCertificates = cerArray;
       
        SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
        NSCAssert(caRef != nil, @"caRef is nil");
       
        NSArray *caArray = @[(__bridge id)(caRef)];
        NSCAssert(caArray != nil, @"caArray is nil");
       
        OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
        SecTrustSetAnchorCertificatesOnly(serverTrust,NO);
        NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
       
        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        __autoreleasing NSURLCredential *credential = nil;
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([weakSelf.manager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                if (credential) {
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
       
        return disposition;
    }];

上述代码通过给AFHTTPSessionManager重新设置证书验证回调来自己验证证书,然后将自己的证书加入到可信任的证书列表中,即可通过证书的校验。

由于服务端使用.jks是一个证书库,客户端获取到的证书可能不止一本,我这边获取到了两本,具体获取到基本可通过SecTrustGetCertificateCount方法获取证书个数,AFNetworking在evaluateServerTrust:forDomain:方法中,AFSSLPinningMode的类型为AFSSLPinningModeCertificate和AFSSLPinningModePublicKey的时候都有校验服务端的证书个数与客户端信任的证书数量是否一样,如果不一样的话无法请求成功,所以这边我就修改他的源码,当有一个校验成功时即算成功。

当类型为AFSSLPinningModeCertificate时

return trustedCertificateCount == [serverCertificates count] - 1;

为AFSSLPinningModePublicKey时

return trustedPublicKeyCount > 0 && ((self.validatesCertificateChain) || (!self.validatesCertificateChain && trustedPublicKeyCount >= 1));

去掉了第二块中的trustedPublicKeyCount == [serverCertificates count]的条件。

这边使用的AFNetworking的版本为2.5.3,如果其他版本有不同之处请自行根据实际情况修改。

demo地址:https://github.com/fengling2300/networkTest

相关文章

网友评论

  • 利利feint:复习来了
  • zziazm:在didReceiveChallenge这个代理方法里面,我把 NSLog(@"信任该证书");之前的验证过程去掉,只调用后面的:
    NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
    completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    return [[challenge sender] useCredential: credential
                              forAuthenticationChallenge: challenge];这个请求能正常进行吗
  • 梁森的简书:发现不配置证书也可以啊
  • Cherish183:客户端不是可以跟服务端共用一个证书么 ? 为什么还要自建证书 ?
  • MR_詹:现在出现这样不可思议的问题:服务器使用的Http,App是按照上面代码写的AFNetworking,正常来说,请求数据是不可能成功的,但是现在依然可以请求数据。楼主遇到这样的问题吗?
    浪投王:@MR_詹 使用http不需要证书啊 当然可以请求到
  • MR_詹:博主,请问ca.cer 证书就是服务器提供给我的证书吗?还有你上面的demo对于https单向认证是否适用:grin:
  • 十一岁的加重:首先把前面生成的.cer文件添加到项目中,注意在添加的时候选择要添加的targets。

    这一步太重要了,一直在调试,老是找报错,原来 是这个原因
  • 6a948902fef0:"一种是花钱向认证的机构购买的证书,服务端如果使用的是这类证书的话,那一般客户端不需要做什么,用HTTPS进行请求就行了"

    求教一下,这种受信任的证书是不是把请求url改成“Https”就行了?其他的需要做什么么?
    浪投王:@wangKy 服务端要支持https,客户端基本不用干啥
  • 小呆鸟:自制一个批处理文件,添加如下命令 这个是什么意思呢 我已经导出P12了 后面这一步不知道怎么处理了 楼主请赐教
  • 奶茶007:你好,我自己搭建了服务器,也生成了证书,下载并修改了demo,但是报错 2016-12-16 15:17:53.048 NetworkTest[4436:178546] 证书认证
    2016-12-16 15:17:53.053 NetworkTest[4436:178546] stutas:0
    2016-12-16 15:17:53.053 NetworkTest[4436:178546] Result: 5
    2016-12-16 15:17:53.053 NetworkTest[4436:178546] error
    2016-12-16 15:17:53.054 NetworkTest[4436:178546] 请求完成
    奶茶007:@奋蚁zcf 那个comonname 那个字段很重要,要写域名
    tinctorial:我也遇到了同样的情况,怎么处理呢 :pray:
    念_安:@奶茶007 stutas:0 Result: 5 这个你怎么处理的 急求
  • KeepFighting:请问,2017年开始, https使用自签名证书可以通过appstore审核吗?
    浪投王:应该是可以的,苹果应该会要求去掉配置文件中允许HTTP的配置项,去掉之后还能请求应该就没问题了。
  • Reulter:博主,证书地址换了。<key>NSAppTransportSecurity</key>
    <dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>里面不改true就会报-1200的错误是为什么呢?
  • hehtao:测试了下,我拿到的是三张证书,博主最后说要改一段源码,没看懂什么意思,在哪里改呢?
    不会打滚儿的狮子:@浪投王 抱歉登错帐号了,那个马尾松 就是前面那个hehtao :blush:
    不会打滚儿的狮子:@浪投王 NSURLSession 是OK的,AFN 我在源码里打断电观察了一下,也没发现什么问题,现在用AFN一直报错 999,没有发现重复请求,请求对象也是活着的,明天我在研究一下AFN的验证机制,谢谢了
    浪投王:@hehtao 你在afnetworking源码里验证证书的方法里打断点看一下
  • 5045a40d0a0c:博主你好,我验证你的证书没问题 但是验证后端提供的.cer证书就崩溃了 都是自制免费的吗
    霖溦:@走马zm http://www.jianshu.com/p/74465973da5e这里有解
    不会打滚儿的狮子:@走马zm 证书有问题,我也碰到过, 报错大概是数组里插入nil了
    浪投王:@走马zm 是自制免费的
  • _子墨:由于java.lang.NullPointException错误的原因,本地虽然生成了.cer文件,但却是个空文件,请博主指教啊!!!!
    aec297d4c995:我也遇到这个问题了,请问你解决了吗?
  • _子墨:博主你好!我在运行你的脚本命令的时候,在下面这一句出了错:
    keytool -gencert -alias ca -storepass jacedy -infile 192.168.1.12\server-192.168.1.12.certreq -outfile 192.168.1.12\server-192.168.1.12.cer -validity 36500 -keystore ca.jks -keypass
    报:keytool错误:java.lang.NullPointException
    不知道问题出在哪???(前面几个命令没问题)
    求助啊……………………
    aec297d4c995:@旭丶Joy 你怎么解决的啊
    _子墨:@旭丶Joy 没有,后来我干脆上淘宝买了一个证书,反正又不贵
    旭丶Joy:@JK_云墨 求助我也是报了这个错误.您解决了吗
  • FLYc:你好,使用自制证书iOS客户端只需要导入包含公钥的证书就行了吗?
    念_安:status = SecTrustEvaluate(serverTrust, &result);这一步返回了这个kSecTrustResultRecoverableTrustFailure 这个问题是怎么处理的
    FLYc:@浪投王 status = SecTrustEvaluate(serverTrust, &result);这一步返回了这个kSecTrustResultRecoverableTrustFailure值你遇到过这个情况吗
    浪投王:@FLYc 是的,用.cer的后缀
  • 1ea6ca9222a5:博主你好, demo运行 提示caRef is nil 证书导入了 服务器地址也换了。 这是什么原因啊
    1ea6ca9222a5:博主,还有个问题请教一下 ,自签名的证书上线的时候 客户端 Info.plist里边 <key>NSAppTransportSecurity</key>
    <dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    这个需要还添加吗?
    1ea6ca9222a5:替换了啊,真心不知道为什么、
    浪投王:@shuaiOS 证书的名字与代码里面的一样吗
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"ca" ofType:@"cer"];//自签名证书
    这边的ca是你那本.cer证书的名称
  • 床前明月_光:你好,博主,我下载了你的demo,什么都没改, 直接运行, 但是没有来到
    - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
    completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
    NSLog(@"证书认证");
    这个方法,请问是什么原因.
    浪投王:@床前明月_光 我demo的证书是我自己做的,请求链接是我自己搞的服务,当然用不了。
    床前明月_光:有证书,自制的,但是后台他不懂什么样的证书才符合要求,你Demo的证书和请求路径好像也用不了
    浪投王:@床前明月_光 你请求的服务器有证书吗,协议是1.2的吗
  • Python数据分析实战:证书转换的时候没有转成功,您能写详细点吗
  • Python数据分析实战:博主 请教一下,项目里还能出现 "http"吗, 是把所有的网络请求都要改成https吗,那第三方(友盟SDK,微信SDK, SDWebImage等 )里的http也要改为https吗 ? 十分感谢
    hehtao:@SilenceYaung ATS 关闭、可以设置例外的域名用http访问
    浪投王:@SilenceYaung 如果把plist文件中的NSAllowsArbitraryLoads设置成NO,那项目中所有的http请求就都不能用了,这也是苹果要求的。
  • Royvonne:博主 问个问题,自签名证书安装在mac上的话,是黄色的还是蓝色的 公司的后台弄了个证书给我,是黄色的,提示语大概是该证书还没经过第三方验证. 但是在safari还是可以请求到,但我换到项目中就不行了 这情况到底是我代码问题呢,还是后台问题?
    浪投王:@Royvonne 项目中请求不到是什么现象,进到证书校验的代理方法里面了吗。
  • sock:dome 请求都是失败
    浪投王:@sock 证书换成你们自己的证书了吗
  • oking:想请问一下,-URLSession:didReceiveChallenge:completionHandler:代理方法不调用是什么原因,需要怎样处理?
    浪投王:@oking 证书校验的回调不走的话注意下服务端的TLS是否是1.2的
    oking:@浪投王 不好意思,我没表达清楚。我下载了您Github上的Demo,在-URLSession:didReceiveChallenge:completionHandler:方法打了断点但是经常不走,而是直接“请求完成”或failure。我重新做了一个自己的Demo,但是在证书认证时SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert)方法返回的caRef为空。想留您个联系方式详细请假一下,我的邮箱是oking.lee@iCloud.com。 :blush:
    浪投王:@oking delegate设置成self了吗
  • d312cc476a2f:不错不错
  • 2d15f6a3b2a2:感觉能用上
  • 某非著名程序员:这个可以有。现在公司是花钱的,觉得以后肯定可以用😀

本文标题:iOS使用自签名证书实现HTTPS请求

本文链接:https://www.haomeiwen.com/subject/rsiqyttx.html