本文讲解了如何使用AFNetworking实现HTTPS支持的不同方式,以及对AFNetworking实现源码分析。

Demo地址

实现默认方式支持HTTPS

首先恭喜你,无论你使用NSURLConnection,NSURLSession还是AFNetworking,默认已经支持了HTTPS,不用写任何代码。当然这也需要一些小的前提,服务端证书是有效证书,并且包含所请求的域名。但如果是自签名方式,则需要自己写代码实现。

实现自签名证书验证支持HTTPS

这里用12306网站来做测试,因为它是用的自签名证书。说个题外话,12306为什么用自签名证书?有兴趣的同学可以看下这里这里。由于12306网站不支持Forward secrecy,所以需要在info.plist中设置NSExceptionRequiresForwardSecrecyNO,具体设置参见Demo

1、把下载的12306根证书导入项目,加载证书文件

1
2
3
4
5
6
- (NSData *)loadCertificateData {
NSString *cerName = @"SRCA"; // 12306根证书
NSString *cerPath = [[NSBundle mainBundle] pathForResource:cerName ofType:@"cer"];
NSData *certData = [NSData dataWithContentsOfFile:cerPath];
return certData;
}

2、修改AFSecurityPolicy对象配置

1
2
3
4
5
6
7
8
9
- (AFSecurityPolicy *)certificateSecurityPolicy {
// 设置验证模式
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
// 设置验证证书
securityPolicy.pinnedCertificates = @[[self loadCertificateData]];
// 设置允许无效证书
securityPolicy.allowInvalidCertificates = YES;
return securityPolicy;
}

3、使用AFNetworking对象请求数据,链接地址为https://kyfw.12306.cn/otn/

1
2
3
4
5
6
7
8
9
10
11
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.securityPolicy = [self certificateSecurityPolicy];
NSString *httpsURL = @"https://kyfw.12306.cn/otn/";
[manager GET: httpsURL parameters:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
[self printLogWithTitle:@"AFNetworking Data:" content:string];
[self printLogWithTitle:@"AFNetworking Finish" content:@""];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[self printLogWithTitle:@"TrustEvaluate Error" content:error];
}];

AFNetworking源码分析

在AFSecurityPolicy中有两个开关:

1
2
3
4
5
// 是否允许无效证书,默认是NO
@property (nonatomic, assign) BOOL allowInvalidCertificates;
// 是否验证域名,默认是YES
@property (nonatomic, assign) BOOL validatesDomainName;

还有三种验证模式:

1
2
3
4
5
6
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone, // 默认验证模式
AFSSLPinningModePublicKey, // 公钥验证模式,比对证书公钥
AFSSLPinningModeCertificate, // 证书验证模式,比对证书内容
// 注意后两种模式都需要导入证书文件
};

我们可以利用他们来实现不同的验证策略,比如,实现自签名证书验证方式,allowInvalidCertificates设为YES,模式选择AFSSLPinningModeCertificate,就可以了。

其中验证逻辑的核心方法是evaluateServerTrust,上半部分为默认验证逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
// 需要验证域名
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 设置策略
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (self.SSLPinningMode == AFSSLPinningModeNone) {
// 不用证书验证时,要么允许无效证书,要么证书有效
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
// 其它模式验证时,证书无效同时不允许无效时,不再继续验证
return NO;
}

下半部分为不同模式下的验证逻辑,证书验证需要设置锚点证书(Anchor Certificate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone:
default:
return NO;
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 设置锚点证书
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
// 取得证书链数组
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
// 逐一比对是否包含证书
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
// 取得公钥数组
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
// 逐一对比证书公钥
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
}

AFServerTrustIsValid中实际就是调用SecTrustEvaluate进行验证评估

1
2
3
4
5
6
7
8
9
10
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
BOOL isValid = NO;
SecTrustResultType result;
__Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
_out:
return isValid;
}

在URLSession的代理中处理验证逻辑,具体实现细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 调用evaluateServerTrust进行验证逻辑
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 生成NSURLCredential凭证credential
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
// 使用disposition和credential完成验证挑战
completionHandler(disposition, credential);
}
}

总结整体流程

  1. 在Delegate中处理验证挑战
  2. 设置验证策略,锚点证书等
  3. 进行信任评估SecTrustEvaluate
  4. 生成凭证credential和disposition
  5. 完成或取消验证挑战

参考资料

  1. Making HTTP and HTTPS Requests
  2. HTTPS Server Trust Evaluation
  3. NSAppTransportSecurity
  4. iOS安全系列之一:HTTPS