一直有用戶反映,不管通過通過手機端、還是PC端訪問我們的產品都會不定時出現域名劫持的問題。為了解決這個問題,我們只能繞過傳統的運營商域名解析,通過IP直接訪問服務。本文對App中集成HttpDNS作簡要介紹。
一、HTTPDNS介紹:
httpDNS是阿里提供的面向移動端的域名解析產品,提供了面向移動端的SDK,客戶端可以通過傳入域名的方式調用,SDK會直接返回解析出的IP地址。
NSString *ip = [[HttpDnsService sharedInstance] getIpByHostAsync:Domain];
二、需求描述:
對項目中: 1.原生圖片的請求, 2.H5網頁中請求,分別做IP直連處理。
三、實現方案:
- 實現原理:
通過注冊NSURLProtocol,攔截所有請求,過濾出相應的圖片請求及H5網頁請求,將請求的url中的域名替換為IP后,重新發起請求,獲取到響應數據后,回調給URL Loading System。
- 實現過程:
1.攔截請求
由于原生圖片和H5網頁中的請求需要分開處理以便于實現通過降級開關分別控制,所以注冊了兩個NSURLProtocol分別處理這兩項業務,具體策略為:
YH_ImageProtocol在攔截到請求后,按照URL后綴(是否包含:.jpg/.jpeg/.png/.gif)過濾出圖片的URL。
YH_WebProtocol在攔截到請求后,排除圖片的URL,則認為是需要攔截的請求。
2. 手動發起請求
攔截到請求后,需要根據協議分別做處理:如果是HTTP請求,使用NSURLSession
重新發起請求,獲取到響應的數據后,回調給URL Loaidng System
。如果是HTTPS
請求,由于當前請求URL的域名被替換成了IP地址,請求URL中的host也會被替換成HTTPDNS解析出來的IP,導致服務器獲取到的域名為解析后的IP,無法找到匹配的證書,只能返回默認的證書或者不返回,所以會出現SSL/TLS握手不成功的錯誤。為了解決這個問題,我們需要hook HTTPS訪問前SSL連接過程,根據網絡請求頭部域中的HOST信息,設置SSL連接PeerHost的值,之后根據服務器返回的證書執行驗證過程。所以在攔截網絡請求后,使用CFHTTPMessageRef
創建NSInputStream
實例進行Socket通信,并設置其kCFStreamSSLPeerName
的值:
// 創建CFHTTPMessage對象的輸入流
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault,cfrequest);
inputStream = (__bridge_transfer NSInputStream *) readStream;
// 設置SNI host信息
NSString *host = [curRequest.allHTTPHeaderFields objectForKey:@"host"];
if (!host) {
host = curRequest.URL.host;
}
[inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys:
host, (__bridge id) kCFStreamSSLPeerName,
nil];
[inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
[inputStream setDelegate:self];
3.重定向
當返回的StatusCode在300、400之間,且header中location字段中取出合法的URL時,用該URL初始化新的請求,在protocol內部重新執行一遍之前的流程。
四、碰到的問題及解決方法:
-
GZIP
之前在測試過程中發現,用Webview加載官網時,頁面顯示亂碼。經排查,確認是返回的content-type
為gzip,因為未解壓導致頁面無法識別。為此,我們在收到響應后,先判斷content類型,如果為gzip,先進行解壓再回調給相應的client。
-
CSS文件中通過相對路徑的方式引用的靜態資源無法加載
該問題發生的具體原因是:在WebView中的請求被攔截,域名改為IP直連后,CSS文件中通過相對路徑引用的靜態資源(包括iconfont和少量圖片)的url直接沿用了CSS文件URL中的IP地址作為域名,跳過了域名解析的步驟,且header中的HOST字段未設置為相應的域名。最終導致無法通過SNI擴展的方式獲取到SSL證書,建連失敗。我們的解決方案是保存好IP地址和域名的映射關系,碰到前述問題時,能夠獲取到IP地址對應的域名,設置給HOST,以保證SSL握手成功。
五、待改進的地方:
目前的業務需求是,攔截到的H5請求,全部強制轉為HTTPS方式請求。這種情況下會導致一些服務端不支持HTTPS的請求失敗,尤其跳轉到一些第三方網站的頁面。為避免該問題,我們應該提供一種容錯機制,當強制使用HTTPS的方式去打開頁面時,如果SSL握手失敗,可以再改為HTTP的方式去請求。