全部產品
Search
文件中心

Alibaba Cloud DNS:iOS端HTTP情境,HTTPS(含SNI)業務情境“IP直連”方案說明

更新時間:Sep 16, 2025

本文主要介紹HTTP情境,HTTPS(含SNI)業務情境下在iOS端實現“IP直連”的解決方案。

概述

HTTP 是一種用於分布式、協作式和超媒體資訊系統的應用程式層協議,是全球資訊網資料通訊的基礎。它通過用戶端-伺服器模型工作,允許瀏覽器向 Web 服務器請求網頁內容,並將結果展示給使用者。HTTP 以明文形式傳輸資料,不提供加密機制,因此在傳輸過程中容易受到竊聽、篡改或中間人攻擊。

HTTPS是一種通過電腦網路進行安全通訊的傳輸協議,經由HTTP進行通訊,利用SSL/TLS建立全通道,加密資料包。HTTPS使用的主要目的是提供對網站伺服器的身份認證,同時保護交換資料的隱私與完整性,TLS是傳輸層加密協議,前身是SSL協議。HTTPS下有兩種業務情境普通情境和SNI情境。

SNI(Server Name Indication)用來改善伺服器與用戶端 SSL(Secure Socket Layer)和 TLS(Transport Layer Security)的擴充,主要解決一台伺服器能夠提供多個網域名稱服務 (DNS)的情況。

  • HTTP情境

    HTTP情境網路鏈路中不存在SSL/TLS握手,無需認證校正,直接將請求URL中的host替換成IP,在HTTP Header中設定Host為原始網域名稱即可。

  • 普通HTTPS情境

    普通HTTPS情境使用“IP直連”對開發人員來說很方便,直接將請求URL中的host替換成IP,在HTTP Header中設定Host為原始網域名稱,在執行認證驗證時將IP再替換成原來的網域名稱即可。

  • SNI情境

    SNI(單IP多HTTPS認證)情境下,iOS上層網路程式庫NSURLConnection/NSURLSession沒有提供介面進行SNI欄位的配置,因此需要Socket層級的底層網路程式庫例如CFNetwork,來實現IP直連網路請求適配方案。而基於CFNetwork的解決方案需要開發人員考慮資料的收發、重新導向、解碼、緩衝等問題(CFNetwork是非常底層的網路實現),希望開發人員合理評估該情境的使用風險,我們推薦開發人員參考iOS14原生加密DNS方案解決SNI情境問題。

重要

如果使用者在Server端使用了CDN服務,請參考SNI情境方案。

實踐方案

  • HTTP情境解決方案

1,直接將請求URL中的host替換成IP

2,在HTTP Header中設定Host為原始網域名稱即可

//構造請求
- (NSMutableURLRequest *)createRequest {
    //假設網域名稱為example.com,從HTTPDNS解析出的ip為1.2.3.4
    NSString *urlString = @"http://example.com/api";
    //1,直接將請求URL中的host替換成IP
    NSString *httpDnsString = [urlString stringByReplacingOccurrencesOfString:@"example.com" withString:@"1.2.3.4"];
    NSURL *httpDnsURL = [NSURL URLWithString:httpDnsString];
    NSMutableURLRequest *mutableReq = [NSMutableURLRequest requestWithURL:httpDnsURL];
    //2,在HTTP Header中設定Host為原始網域名稱
    [mutableReq setValue:@"example.com" forHTTPHeaderField:@"Host"];
    return mutableReq;
}

  • HTTPS下普通情境解決方案

1,直接將請求URL中的host替換成IP

2,在HTTP Header中設定Host為原始網域名稱

3,針對認證驗證時“domain不匹配”問題,可以採用如下方案解決:執行認證驗證時將IP直接替換成原來的網域名稱。

此樣本針對NSURLSession/NSURLConnection介面。

//構造請求
- (NSMutableURLRequest *)createRequest {
    //假設網域名稱為example.com,從HTTPDNS解析出的ip為1.2.3.4
    NSString *urlString = @"https://example.com/api";
    //1,直接將請求URL中的host替換成IP
    NSString *httpDnsString = [urlString stringByReplacingOccurrencesOfString:@"example.com" withString:@"1.2.3.4"];
    NSURL *httpDnsURL = [NSURL URLWithString:httpDnsString];
    NSMutableURLRequest *mutableReq = [NSMutableURLRequest requestWithURL:httpDnsURL];
    //2,在HTTP Header中設定Host為原始網域名稱
    [mutableReq setValue:@"example.com" forHTTPHeaderField:@"Host"];
    return mutableReq;
}

//3認證驗證時將IP直接替換成原來的網域名稱

/*
 * NSURLConnection
 */
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if (!challenge) {
        return;
    }
    /*
     * URL裡面的host在使用HTTPDNS的情況下被設定成了IP,此處從HTTP Header中擷取真實網域名稱
     */
    NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = self.request.URL.host;
    }
    /*
     * 判斷challenge的驗證方法是否是NSURLAuthenticationMethodServerTrust(HTTPS模式下會進行該身分識別驗證流程),
     * 在沒有配置驗證方法的情況下進行預設的網路請求流程。
     */
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
    {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            /*
             * 驗證完以後,需要構造一個NSURLCredential發送給發起方
             */
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
        } else {
            /*
             * 驗證失敗,進入預設處理流程
             */
            [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
        }
    } else {
        /*
         * 對於其他驗證方法直接進行處理流程
         */
        [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
    }
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
/*
 * NSURLSession
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
    if (!challenge) {
        return;
    }
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;
    /*
     * 擷取原始網域名稱資訊。
     */
    NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = self.request.URL.host;
    }
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    // 對於其他的challenges直接使用預設的驗證方案
    completionHandler(disposition,credential);
}	

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /*
     * 建立認證校正策略
     */
    NSMutableArray *policies = [NSMutableArray array];
    if (domain) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    /*
     * 綁定校正策略到服務端的認證上
     */
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
    /*
     * 評估當前serverTrust是否可信任,
     * 官方建議在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed
     * 的情況下serverTrust可以被驗證通過,https://developer.apple.com/library/ios/technotes/tn2232/_index.html
     * 關於SecTrustResultType的詳細資料請參考SecTrust.h
     */
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}

普通情境下AFNetworking網路程式庫下的IP直連,參考如下方案解決:

//使用該傳回值作為AFHTTPSessionManager請求的URL即可
+(NSString *)getIPStringFromAliCloudDNSResolverWithURLString: (NSString *)URLString manager: (AFHTTPSessionManager *)manager {
    NSURL *originUrl = [NSURL URLWithString :URLString];
    NSString *host = originUrl.host;
    NSString *ip= [[DNSResolver share] getIpsByCacheWithDomain:host andExpiredIPEnabled:YES].firstObject;
    NSString *ipURLString = URLString;
    if (ip) {
        //1,直接將請求URL中的host替換成IP
        ipURLString = [URLString stringByReplacingOccurrencesOfString:host withString:ip];
        //2,在HTTP Header中設定Host為原始網域名稱
        [manager.requestSerializer setValue:originUrl.host forHTTPHeaderField:@"Host"];
        __weak typeof (AFHTTPSessionManager *) weakSessionManager = manager;
        //3認證驗證時將IP直接替換成原來的網域名稱
        [manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession*_Nonnullsession,
                                                                                                        NSURLAuthenticationChallenge * _Nonnull challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential) {
            NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
            //擷取原始網域名稱資訊
            NSString *host = [[weakSessionManager.requestSerializer HTTPRequestHeaders] objectForKey:@"host" ];
            if (!host) {
                host = challenge.protectionSpace.host;
            }
            if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
                if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
                    disposition = NSURLSessionAuthChallengeUseCredential;
                    *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                disposition = NSURLSessionAuthChallengePerformDefaultHandling;
            }
            return disposition;
        }];
    }
        return ipURLString;
}

+(BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
     NSMutableArray *policies = [NSMutableArray array];
     if (domain) {
         [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
     } else {
         [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
     }
     SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
     SecTrustResultType result;
     SecTrustEvaluate(serverTrust, &result);
     return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
 }
 

重要

基於該方案發起網路請求,若報出SSL校正錯誤,比如iOS系統報錯kCFStreamErrorDomainSSL, -9813; The certificate for this server is invalid,請檢查應用情境是否為SNI(單IP多HTTPS網域名稱)。

  • HTTPS下SNI情境解決方案

1. 自訂NSURLProtocol方案

SNI情境下的解決方案請參考Demo樣本工程源碼,下面將介紹目前面臨的一些挑戰,以及應對策略:

支援POST請求

使用NSURLProtocol攔截NSURLSession請求丟失body,解決方案如下:

使用HTTPBodyStream擷取body,並賦值到body中,具體的代碼如下,可以解決上面提到的問題:

//
//  NSURLRequest+NSURLProtocolExtension.h
//
//
#import <Foundation/Foundation.h>
@interface NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)alidns_getPostRequestIncludeBody;
@end
//
//  NSURLRequest+NSURLProtocolExtension.h
//
//
#import "NSURLRequest+NSURLProtocolExtension.h"
@implementation NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)alidns_getPostRequestIncludeBody {
    return [[self alidns_getMutablePostRequestIncludeBody] copy];
}
- (NSMutableURLRequest *)alidns_getMutablePostRequestIncludeBody {
    NSMutableURLRequest * req = [self mutableCopy];
    if ([self.HTTPMethod isEqualToString:@"POST"]) {
        if (!self.HTTPBody) {
            NSInteger maxLength = 1024;
            uint8_t d[maxLength];
            NSInputStream *stream = self.HTTPBodyStream;
            NSMutableData *data = [[NSMutableData alloc] init];
            [stream open];
            BOOL endOfStreamReached = NO;
            //不能用 [stream hasBytesAvailable]) 判斷,處理圖片檔案的時候這裡的[stream hasBytesAvailable]會始終返回YES,導致在while裡面死迴圈。
            while (!endOfStreamReached) {
                NSInteger bytesRead = [stream read:d maxLength:maxLength];
                if (bytesRead == 0) { //檔案讀取到最後
                    endOfStreamReached = YES;
                } else if (bytesRead == -1) { //檔案讀取錯誤
                    endOfStreamReached = YES;
                } else if (stream.streamError == nil) {
                    [data appendBytes:(void *)d length:bytesRead];
                }
            }
            req.HTTPBody = [data copy];
            [stream close];
        }
    }
    return req;
}
@end

使用方法:

在用於攔截請求的NSURLProtocol的子類中實現方法+canonicalRequestForRequest:並處理request對象:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return [request alidns_getPostRequestIncludeBody];
}

下面介紹相關方法的作用:

//NSURLProtocol.h
/*!
 *  @method: 建立NSURLProtocol執行個體,NSURLProtocol註冊之後,所有的NSURLConnection都會通過這個方法檢查是否持有該Http請求。
 @param:
 @return: YES:持有該Http請求       NO:不持有該Http請求
 */
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
/*!
 *  @method: NSURLProtocol抽象類別必須要實現。通常情況下這裡有一個最低的標準:即輸入輸出請求滿足最基本的協議規範一致。因此這裡簡單的做法可以直接返回。一般情況下我們是不會去更改這個請求的。如果你想更改,比如給這個request添加一個title,組合成一個新的http請求。
 @param: 本地HttpRequest請求:request
 @return: 直接轉寄
 */
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest *)request

簡單說:

  • +[NSURLProtocol canInitWithRequest:]負責篩選哪些網路請求需要被攔截

  • +[NSURLProtocol canonicalRequestForRequest:]負責對需要攔截的網路請求NSURLRequest進行重新構造。

這裡有一個注意點:+[NSURLProtocol canonicalRequestForRequest:]的執行條件是+[NSURLProtocol canInitWithRequest:]傳回值為YES

注意在攔截NSURLSession請求時,需要將用於攔截請求的NSURLProtocol的子類添加到 NSURLSessionConfiguration中,用法如下:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[ [CUSTOMEURLProtocol class] ];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];

SNI情境下AFNetworking網路程式庫下的IP直連,參考如下方案解決:

    // 建立 NSURLSessionConfiguration 執行個體
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    
    // 設定使用自訂的 HTTP DNS 解析協議
    config.protocolClasses = @[[CFHTTPDNSHTTPProtocol class]];

    // 初始化 AFHTTPSessionManager
    AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:config];
    sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];

    // 建立資料任務
    NSURLSessionDataTask *task = [sessionManager dataTaskWithHTTPMethod:@"GET"
                                                             URLString:@"您的請求URL"
                                                            parameters:nil
                                                               headers:nil
                                                            uploadProgress:^(NSProgress *uploadProgress) {
        NSLog(@"Upload progress: %@", uploadProgress.localizedDescription);
    } downloadProgress:^(NSProgress *downloadProgress) {
        NSLog(@"Download progress: %@", downloadProgress.localizedDescription);
    } success:^(NSURLSessionDataTask *task, id responseObject) {
        // 處理成功響應
        NSLog(@"Success: %@", responseObject);
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        // 處理錯誤
        NSLog(@"Failure: %@", error.localizedDescription);
    }];
    
    // 啟動任務
    [task resume];

2. 其他底層網路程式庫方案

libcurl為例,libcurl / cURL至少7.18.1(2008年3月30日)在SNI支援下編譯一個 SSL/TLS 工具包,curl中有一個--resolve方法可以實現使用指定IP訪問HTTPS網站。

在iOS實現中,代碼如下:

// {HTTPS網域名稱}:443:{IP地址}
NSString *curlHost = ...;
_hosts_list = curl_slist_append(_hosts_list, curlHost.UTF8String);
curl_easy_setopt(_curl, CURLOPT_RESOLVE, _hosts_list);

其中curlHost形如:{HTTPS網域名稱}:443:{IP地址}

_hosts_list是結構體類型hosts_list,可以設定多個IP與Host之間的映射關係。curl_easy_setopt方法中傳入CURLOPT_RESOLVE將該映射設定到 HTTPS 請求中。這樣就可以達到設定SNI的目的。

總結

方案

適用情境

優點

缺點

1, 僅替換URL

2,設定Host

HTTP情境

最簡單

僅適合HTTP明文傳輸協議

1, 替換URL

2,設定Host

3,認證驗證時將IP直接替換成原來的網域名稱

HTTPS普通情境(非SNI情境)

整合簡單,適用系統常見網路程式庫,NSURLSession,

NSURLConnection,

AFHTTPSessionManager

不支援SNI

自訂NSURLProtocol

全部情境

完全基於系統底層API

基於底層網路程式庫,開發維護成本高,需要自己處理收發、重新導向、解碼、緩衝等問題

libcurl

全部情境

跨平台

社區成熟,文檔豐富,

可設定 SNI,

跨平台相容性強

C語言介面對於iOS開發人員有一定的學習成本

需要自己處理Cookie、重新導向、緩衝等問題