移動互聯網的網絡情況是十分複雜的,三大運營商、3G、4G、Wi-Fi、地點等任何一個狀態的改變都會致使網絡情況的變化,而且運營商、代理商們還可能在其中搞一些小破壞,好比常常會有用戶反饋說某個頁面訪問不了或者返回結果不正確等問題,這種情況通常都是發生了域名劫持,通用的解決方案就是使用 IP 直連,跨過運營商 LocalDNS 服務器解析過程,從而達到下降延遲、避免劫持的效果。html
爲何你們會選擇直接使用 IP 來進行鏈接呢?它具備多方面的優點:ios
防劫持,能夠繞過運營商 LocalDNS 解析過程,避免域名劫持,提升網絡訪問成功率git
下降延遲,DNS 解析是一個相對耗時的工做,跳過這個過程能夠下降必定的延遲github
精準調度,運營商解析返回的節點不必定是最優的,本身獲取 IP 能夠基於本身的策略來獲取最精準的、最優的節點緩存
對於獲取 IP,我瞭解到的是兩種方案,一種是直接接入騰訊或者阿里的 HTTPDNS 服務,在發起請求的時候是經過 HTTPDNS 獲取 IP,而後直接使用 IP 來進行業務訪問;一種是內置 Server IP,能夠在啓動等階段由服務端下發域名和 IP 的對應列表,客戶端來進行緩存,發起網絡請求的時候直接根據緩存 IP 來進行業務訪問。安全
起初我是存在一些疑問的,爲何 HTTPS 還會存在 DNS 劫持問題呢?HTTPS 從身份認證、內容加密、防止篡改三個方面來保證網絡安全,可是它的時機相對靠後,DNS 解析是網絡請求的第一個步驟,完整的步驟是 DNS 解析 -> TCP 鏈接 -> TLS 握手 -> Request -> Response,因此 HTTPS 對 DNS 劫持也無能爲力,可是能夠經過客戶端身份認證來避免被塞廣告等情況的發生,被劫持後直接訪問失敗。bash
HTTPS 網絡下咱們會對服務端的身份進行認證,就拿 AFNetworking 來講,它提供了三種身份認證的可選方案:服務器
AFSSLPinningModeNone(Default)網絡
AFSSLPinningModePublicKeydom
AFSSLPinningModeCertificate
對於 AFSSLPinningModeNone,客戶端本身不會對服務端證書進行自主校驗,而是直接默認信任,將證書交給系統來進行校驗,判斷返回的證書是不是官方機構頒發的,若是是則信任,這個應該也是普通開發者採用最多的方案。這種認證方案若是被劫持後返回的證書也是官方機構頒發的,那麼客戶端就會正常進行網絡訪問,可是返回的數據就不是想要的數據,好比被塞廣告等現象的出現。
對於 AFSSLPinningModePublicKey、AFSSLPinningModeCertificate 兩種方案,客戶端本地一般會保存一份服務端證書, AFSSLPinningModePublicKey 表明客戶端會將服務器端返回的證書與本地保存的證書中的 PublicKey 部分進行自主校驗,AFSSLPinningModeCertificate 表明客戶端會將服務器端返回的證書和本地保存的證書中的全部內容,包括 PublicKey 和證書部分所有進行自主校驗。若是校驗成功,才繼續進行系統驗證等後續行爲。
雖然 Apple 很早就開始推 ATS,可是因爲國內的網絡狀況特別複雜,因此 Apple 仍是支持 HTTP 網絡請求的,而且 HTTP 協議下的網絡請求的比例也很大。實現 HTTP 協議下 IP 鏈接實際上是很簡單的,咱們只須要經過 NSURLProtocol 來攔截網絡請求,而後將符號條件的網絡請求 URL 中的域名修改成 IP 就能夠啦。
可是也會存在一些小問題,域名置換爲 IP 以後,服務端沒法根據 URL 來判斷你要訪問哪一個域名,因此咱們須要手動的將 host 字段塞到 header 中去,方便服務器的正確識別。
HTTPS 比 HTTP 多了一個 TLS 握手的過程,這個過程當中涉及到一個證書校驗的問題,服務端會在第二次握手的時候返回一個證書,來使得客戶端能夠校驗它的身份,避免出現假冒的情況。在此次校驗過程當中,會校驗證書的 domain 域是否包含本次 Request 的 host,而且校驗返回的證書是不是官方機構頒發的可信證書。因爲 IP 鏈接會將 URL 中的域名置換爲 IP,因此就會致使返回的證書和咱們的 Request 校驗失敗的問題。
爲了解決證書的校驗的問題,咱們須要在證書校驗前,再進行一次域名的替換,此次須要把 URL 中 IP 置換爲域名,這樣證書校驗的問題即可迎刃而解。
一般狀況下咱們的證書是和域名綁定的,有時候會存在一個 IP 對應多個域名的狀況,這種狀況若是客戶端 IP 直連的時候,沒有告訴服務端他要請求的是哪一個域名的證書,服務端就沒辦法返回正確的證書,從而致使客戶端證書校驗失敗。
SNI 的全稱 Server Name Indication,爲了解決一個服務器使用多個域名和證書的 SSL/TLS 擴展。在鏈接到服務器創建 SSL 鏈接時,客戶端能夠在第一次握手的 Client Hello 中的 SNI 擴展字段中填入要訪問站點的域名(Hostname),這樣服務器就能夠根據域名返回一個合適的證書。
綜上所述,咱們在進行 IP 直連的時候,面對單 IP 多域名的狀況須要客戶端手動配置 SNI 字段,可是上層的網絡庫 NSURLSession、NSURLConnection 都沒有提供配置的接口,咱們須要使用更加底層的 libcurl 庫和 CFNetwork 來完成 SNI 字段的配置。以 CFNetwork 爲例,可使用以下代碼進行 SNI 的配置。
// HTTPS 請求處理 SNI 場景
NSString *host = [self.swizzleRequest.allHTTPHeaderFields objectForKey:@"host"];
if (!host) {
host = self.originalRequest.URL.host;
}
[self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *sslProperties = @{ (__bridge id) kCFStreamSSLPeerName : host };
[self.inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
複製代碼
其實 IP 直連的文章早已數不勝數,本篇也只是一個學習實踐的筆記而已,更多相關資料和代碼我會在末尾給出。NSURLProtocol 和 IP 直連有不少要注意的點,好比 NSURLProtocol 攔截 post 請求獲取 httpbody 爲空、Cookie 的處理、WebView 的處理等。有些知識本人也沒有實踐,不過能夠在參考文章中找到相應的處理方案。