HTTPS(SNI)场景

最近更新时间:2024-03-19 15:51:31

我的收藏

原理

SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的 SSL/TLS 扩展。工作原理如下:
在连接到服务器建立 SSL 连接之前先发送要访问站点的域名(Hostname)。
服务器根据这个域名返回一个合适的证书。
上述过程中,当客户端使用 HTTPDNS 解析域名时,请求 URL 中的 host 会被替换成 HTTPDNS 解析出来的 IP,导致服务器获取到的域名为解析后的 IP,无法找到匹配的证书,只能返回默认的证书或者不返回,所以会出现 SSL/TLS 握手不成功的错误。
由于 iOS 上层网络库 NSURLConnection/NSURLSession 没有提供接口进行 SNI 字段的配置,因此可以考虑使用 NSURLProtocol 拦截网络请求,然后使用 CFHTTPMessageRef 创建 NSInputStream 实例进行 Socket 通信,并设置其 kCFStreamSSLPeerName 的值。

方案描述

注意
本文档提出了 WebView 场景下 HTTPDNS 集成的参考方案,示例代码非线上生产环境正式代码。在接入之前,我们建议您充分评估本文档内容,以确保方案的健壮性符合您的生产标准。
如果您需要自定义 NSURLProtocol,您可以参考 MSDKDnsHttpMessageTools 的 源码
HTTPDNS iOS SDK 提供了 MSDKDnsHttpMessageTools。MSDKDnsHttpMessageTools 是 HTTPDNS iOS SDK 基于 NSURLProtocol 封装的 Protocol。MSDKDnsHttpMessageTools 继承了 NSURLProtocol,可以自动拦截 NSURLSession 中的请求。MSDKDnsHttpMessageTools 解决了自定义 NSURLProtocol 使用的 CFNetwork 库功能受限,扩展性差的问题。

代码示例

完整的示例代码在 Demo 的 SNIViewController.m 中。
// 注册拦截请求的 NSURLProtocol
[NSURLProtocol registerClass:[MSDKDnsHttpMessageTools class]];

// 需要设置 SNI 的 URL,比如 https://www.qq.com
NSString *originalUrl = @"your url";
NSURL *url = [NSURL URLWithString:originalUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];

// NSURLConnection 例子
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[self.connection start];

// NSURLSession 例子
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[ [MSDKDnsHttpMessageTools class] ];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
self.task = [session dataTaskWithRequest:request];
[self.task resume];

// AFNetworking 例子
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[[MSDKDnsHttpMessageTools class]];
config.protocolClasses = protocolArray;
AFHTTPSessionManager* sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:config];
sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSURLSessionDataTask* task = [sessionManager dataTaskWithRequest:request uploadProgress:^(NSProgress * _Nonnull uploadProgress) {
    NSLog(@"update upload progress %@", uploadProgress.description);
} downloadProgress:^(NSProgress * _Nonnull downloadProgress) {
    NSLog(@"update download progress %@", downloadProgress.description);
} completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
    NSLog(@"request complete ==== response: %@ ===== error: %@", [NSString stringWithFormat:@"%@", responseObject], error);
}];
[task resume];

使用说明

需调用以下接口设置需要拦截域名或无需拦截的域名:
#pragma mark - SNI 场景,仅调用一次即可,请勿多次调用
/**
SNI 场景下设置需要拦截的域名列表
建议使用该接口设置,仅拦截 SNI 场景下的域名,避免拦截其它场景下的域名

@param hijackDomainArray 需要拦截的域名列表
*/
- (void) WGSetHijackDomainArray:(NSArray *)hijackDomainArray;

/**
SNI 场景下设置不需要拦截的域名列表

@param noHijackDomainArray 不需要拦截的域名列表
*/
- (void) WGSetNoHijackDomainArray:(NSArray *)noHijackDomainArray;
如设置了需要拦截的域名列表,则仅会拦截处理该域名列表中的 HTTPS 请求,其他域名不做处理。
如设置了不需要拦截的域名列表,则不会拦截处理该域名列表中的 HTTPS 请求。
注意
建议使用 WGSetHijackDomainArray 仅拦截 SNI 场景下的域名,避免拦截其他场景下的域名。