准备证书

首先需要后端提供的证书

CA证书.cer(根证书)

如果后端提供的是.crt格式的根证书,我们需要转换一下:

双击.crt文件添加根证书到钥匙串,然后右键选择导出为.cer

客户端.p12文件

如果后端提供的是.pem,我们需要转换一下:

双击.pem文件添加根证书到钥匙串,然后右键选择导出为.p12文件

OC代码

结合AFNetworking实现了自签证书的双向认证

static AFHTTPSessionManager *baseURLManager = nil;
+ (AFHTTPSessionManager *)baseURLManager
{
    if (!baseURLManager)
    {
        baseURLManager = [AFHTTPSessionManager manager];
        baseURLManager.responseSerializer = [AFHTTPResponseSerializer serializer];
        [baseURLManager.requestSerializer setValue:@"Keep-Alive" forHTTPHeaderField:@"connection"];
        baseURLManager.requestSerializer.timeoutInterval = 10.0f;
        // 设置提交的内容的编码
        [baseURLManager.requestSerializer setValue:@"application/x-www-form-urlencoded; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
    }
    return baseURLManager;
}

+ (NSURLSessionDataTask *)POST:(NSString *)url parameters:(NSDictionary *)parameters success:(void (^)(NSURLSessionDataTask * task, id responseObject))success failure:(void (^)(NSURLSessionDataTask * task, NSError * error))failure {
    
    AFHTTPSessionManager *manager = [self baseURLManager];
    
    NSString *baseUrl = [FHAPITool getBaseURL];
    NSString *finUrl = [baseUrl stringByAppendingString:url];
    
    RLog(@"finurl -- %@", finUrl);
    
    // 配置https验证策略
    if ([finUrl hasPrefix:@"https://"]) {
        [self securityPolicyForHTTPSessionManager:manager];
        
        [[self class] setSessionDidReceiveAuthenticationChallengeWithManager:manager];
    } else {
        manager.securityPolicy = [AFSecurityPolicy defaultPolicy];
    }
    
    NSURLSessionDataTask *dataTask = [manager POST:finUrl parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
        
        if (success) {
            success(task, responseObject);
        }
        
    } failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) {
        RLog(@"error - %@", error);
        if (failure) {
            failure(task, error);
        }
    }];
    
    return dataTask;
}


#pragma mark - privte methods

+ (void)setSessionDidReceiveAuthenticationChallengeWithManager:(AFHTTPSessionManager *)manager{
    
    __weak typeof(manager)weakManager = manager;
    __weak typeof(self)weakSelf = self;
    
    [manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing*_credential) {
        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        __autoreleasing NSURLCredential *credential =nil;
        
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            RLog(@"验证服务器1");
            if ([weakManager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                RLog(@"验证服务器2");
                
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                if (credential) {
                    RLog(@"验证服务器3");
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    RLog(@"验证服务器4");
                    
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                RLog(@"验证服务器5");
                
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            RLog(@"验证客户端1");
            
            // client authentication
            SecIdentityRef identity = NULL;
            SecTrustRef trust = NULL;
            NSString *p12 = [[NSBundle mainBundle] pathForResource:@"client"ofType:@"p12"];
            NSFileManager *fileManager = [NSFileManager defaultManager];
            
            if (![fileManager fileExistsAtPath:p12]) {
                NSLog(@"client.p12:not exist");
            } else {
                RLog(@"验证客户端2");
                
                NSData *PKCS12Data = [NSData dataWithContentsOfFile:p12];
                
                if ([[weakSelf class] extractIdentity:&identity andTrust:&trust fromPKCS12Data:PKCS12Data])
                {
                    RLog(@"验证客户端3");
                    
                    SecCertificateRef certificate = NULL;
                    SecIdentityCopyCertificate(identity, &certificate);
                    const void*certs[] = {certificate};
                    CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
                    credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge  NSArray *)certArray persistence:NSURLCredentialPersistencePermanent];
                    disposition = NSURLSessionAuthChallengeUseCredential;
                }
                
            }
        }
        *_credential = credential;
        return disposition;
    }];
}

/**
 *  配置https验证策略
 */
+ (void)securityPolicyForHTTPSessionManager:(AFHTTPSessionManager *)manager
{
    
    // 安全策略,配置https请求
//    AFSecurityPolicy * securityPolicy = [AFSecurityPolicy defaultPolicy];
    
    AFSecurityPolicy * securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    
    //allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
    //如果是需要验证自建证书,需要设置为YES
    securityPolicy.allowInvalidCertificates = YES;
    
    //validatesDomainName 是否需要验证域名,默认为YES;
    //假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
    //置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
    //如置为NO,建议自己添加对应域名的校验逻辑。
    securityPolicy.validatesDomainName = NO;
    
    manager.securityPolicy = securityPolicy;
    
    // 先导入证书
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"root" ofType:@"cer"];//证书的路径
    
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    securityPolicy.pinnedCertificates = [[NSSet alloc] initWithArray:@[certData]];
}

+ (BOOL)extractIdentity:(SecIdentityRef *)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
    OSStatus securityError = errSecSuccess;
    // client certificate password
    NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:@"p12密码" forKey:(__bridge id)kSecImportExportPassphrase];
    
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data, (__bridge CFDictionaryRef)optionsDictionary, &items);
    
    if (securityError == 0) {
        CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex(items,0);
        const void *tempIdentity = NULL;
        tempIdentity = CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemIdentity);
        *outIdentity = (SecIdentityRef)tempIdentity;
        const void *tempTrust = NULL;
        tempTrust = CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemTrust);
        *outTrust = (SecTrustRef)tempTrust;
    } else {
        NSLog(@"Failedwith error code %d", (int)securityError);
        return NO;
    }
    return YES;
}