????????本期是通過NSURLProtocol攔截的方式替換ip,包括SNI的處理。后期有時間會有一章通過hook網(wǎng)絡(luò)框架AFN的方式,這種方式也無需改變AFN源碼,包括在HTTPS證書校驗的過程。而本文主要針對HTTPS協(xié)議進行說明,HTTP協(xié)議沒有SSL/TLS的證書驗證的過程,處理起來比較簡單,也無需用到CFNetwork,所以暫時不做講解。還有一篇結(jié)合SDWebImage對HTTP協(xié)議進行"ip直連"的文章。無論是HTTP還是HTTPS都可結(jié)合AFNetwork和SDWebImage進行"ip直連",其中原理都是相通的。若有不足或誤區(qū),盼請指正。
? ? ? ? 大概說一下本章內(nèi)容:本文對HTTPDNS做一個較為詳細的講解,在看本文之前,您需要對系統(tǒng)偏底層的CFNetwork網(wǎng)絡(luò)框架有一個比較基礎(chǔ)的認知。整體總結(jié)為以下幾點。
一、什么是HTTPDNS?
? ? ? ? 首先,DNS解析的目的就是通過某個域名:host(www.xxx.com)找到ip地址(111.111.1.1)。對于HTTPDNS的含義,用通俗簡單一點語言解釋。就是跳過運營商的DNS解析,通過ip地址直接精準(zhǔn)對目標(biāo)服務(wù)器進行連接??杀苊獗唤俪?,并大大縮短DNS解析時間,這也是對網(wǎng)絡(luò)請求的一個重要優(yōu)化環(huán)節(jié)(DNS優(yōu)化)。
? ? ? ? 我們都知道一般的request發(fā)出去之后的第一步就是對請求域名進行解析,而一般情況下我們的解析都是走運營商的LocalDNS,首先LocalDNS會對請求過的數(shù)據(jù)做緩存,因為緩存的時效性,我們想拿到最新最準(zhǔn)確的數(shù)據(jù)時就會受限。其次,在這個DNS解析的過程中容易被外界劫持,注入一些未知的內(nèi)容(像廣告一類)后返回,甚至返回一個全新的ip給用戶,可能危及到用戶的個人隱私。而HTTPDNS是讓用戶繞過LocalDNS,在發(fā)送請求之前在本地就把host換成ip,直接對準(zhǔn)目標(biāo)服務(wù)器,這樣就無需去問運營商服務(wù)器索要ip了,本來耗時不短的DNS的解析過程耗已在本地客戶端完成了,所以DNS解析的時間就大大縮短了。? ?
? ? ? ? 至于怎么拿到host對應(yīng)的ip呢,因為客戶端沒法拿到當(dāng)前host對應(yīng)的最優(yōu)ip(比如根據(jù)發(fā)送請求的地域不同等原因,對應(yīng)的最優(yōu)ip也可能不同),所以最好不要寫死在客戶端,而是通過外界拿到。兩種方案:1、服務(wù)器直接下發(fā),此種情況也是比較復(fù)雜,因為涉及的很多容災(zāi)的處理,比如在ip鏈接失敗的時候,需要用LocalDNS做兜底,并向服務(wù)器上傳錯誤日志,詢問是否需要更新本地ip集合等等一系列復(fù)雜的操作。2、通過三方拿到,比如DNSPOD,阿里等。個人建議選擇支持預(yù)處理的三方,在啟動app的時候,預(yù)先拿到需要的ip集合,比如阿里。
二、結(jié)合AFNetwork的普通網(wǎng)絡(luò)請,通過NSURLProtocol對網(wǎng)絡(luò)進行攔截,然后進行"ip直連"。
? ? ? ? 此文主要通過NSURLProtocol攔截請求的方式進行“ip直連”,以及post請求時,body“丟失”問題處理,SNI場景處理,利用CFNetwork對SNI的處理。先簡單介紹一下NSURLProtocol,NSURLProtocol是蘋果提供給開發(fā)者們專門用來劫持網(wǎng)絡(luò)請求的一個抽象類,所以一般用到的時候選擇用繼承NSURLProtocol的方式,本文中的WYCustomURLProtocol就只這樣一個子類。先了解幾個重要的api:? ? ? ?
1、+ (BOOL)canInitWithRequest:(NSURLRequest *)request;// 通過協(xié)議或是域名判斷哪些域名的請求需要攔截的就返YES,反之返NO。? ?
2、+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;? // 上面方法的返YES時,會走到這個方法,你可以直接返回request,也可以重定向一個request返回。
3、- (void)startLoading;// 重定向的request就是從此發(fā)出去的
4、- (void)stopLoading; //? 當(dāng)cancel請求的時候,會調(diào)用此方法。
接下來就開始附上code了,要用UrlProtocol之前需要對之進行注冊。
第1步、在初始化AFHTTPSessionManager的時候,對其進行注冊:如圖 1

第2步、若你使用到了YTK框架,那么可以這樣初始化,此處為了便于理解所以引用了YTK,實際操作的時候,建議對外提供一個注冊接口,不要直接用此種直接依賴TYK的方式,以免污染HTTPDNS部分。 圖 2

第3步、對于要過濾攔截的域名需在此方法判斷 圖 3

第4步、請求ip替換host的過程,并給header添加host, 如圖 4。此處只要能正確替換,大家可以各顯神通。

其中的wy_bodyForPost目的是為了取回body,方法體如圖 5?

????????此處需要說明一下,通過NSURLProtocol攔截的post請求的body并非真的丟失了,只是body數(shù)據(jù)在URL loading system中到達這里之前就已被轉(zhuǎn)成stream了??梢栽趓equest.HTTPBodyStream中解析它。
第5步、利用CFNetwork重新構(gòu)建C語言的request,并發(fā)送。如圖 6和 圖7


三、SNI場景。
????????什么是SNI場景呢,SNI全稱 Server Name Indication,是TLS的擴展。我們知道一個服務(wù)器可能對應(yīng)了多個域名的情況,客戶端與服務(wù)端在建立HTTPS連接的過程中要進行SSL/TLS握手。整個握手過程步驟分5步:
1、客戶端發(fā)起握手請求,攜帶隨機數(shù)、支持算法列表等參數(shù)。
2、服務(wù)端收到請求,選擇合適的算法,下發(fā)公鑰證書和隨機數(shù)。
3、客戶端對服務(wù)端證書進行校驗,并發(fā)送隨機數(shù)信息,該信息使用公鑰加密。
4、服務(wù)端通過私鑰獲取隨機數(shù)信息。
5、雙方根據(jù)以上交互的信息生成session ticket,用作該連接后續(xù)數(shù)據(jù)傳輸?shù)募用苊荑€。
而握手失敗就發(fā)生在第3步,客戶端驗證后端給的證書過程如下:
1、客戶端用本地的根證書解開證書鏈,確認服務(wù)端下發(fā)的證書是由可信任機構(gòu)頒發(fā)。
2、客戶端檢查證書的domain域和擴展域,驗證是否包含本次請求的host。
若是在握手過程中客戶端要是沒有指定需要訪問的目標(biāo)域名,這就會導(dǎo)致一個問題:如果一臺服務(wù)機對應(yīng)了多個域名,每個域名對應(yīng)的證書也不一樣,那服務(wù)端也不知道該返回給客戶端哪一套證書進行校驗,導(dǎo)致第3步的校驗失敗,以至握手失敗。但若是我們指定該SSL/TLS的目標(biāo)域名,那服務(wù)端就知道該返回那一套證書了,即可通過證書驗證,握手成功。
那么如何指定目標(biāo)域名呢?我們可以為SNI擴展字段添加一個host。而iOS的上層網(wǎng)絡(luò)庫NSURLConnection/NSURLSession沒有提供對SNI字段進行配置的接口,所以要用到Socket層級的底層網(wǎng)絡(luò)庫CFNetwork,對SNI字段進行配置。如圖 8

補充:1、為什么要在SNI的擴展里面添加傳host,我不是在header傳了host了嗎?此處做一個簡要說明:由于HTTP的host字段在header中。而SSL/TLS的握手以及證書驗證都是在HTTP開始之前,所以header里面的host對于如何判斷選取證書是沒有關(guān)系的。這個時候,SSL/TLS協(xié)議的HELLO中增加了一個server name字段(SNI擴展字段),讓HTTP的客戶端可以提前設(shè)置host,從而使服務(wù)端在SSL/TLS的握手階段可以選擇正確的證書。在SSL/TLS握手過程中,若不是SNI的情況,意味著服務(wù)器只對應(yīng)了一個域名,一套證書,所以可以直接返回即為正確的證書,這也是為什么不是SNI的情況下無需配置SNI的原因。前面我們說了SNI呢是一對多的情況,在拿不到host的時候不知道返回哪一套證書,就會返回默認的一套或不返回,這個時候我們就需要在配置SNI了,添加server name字段,對應(yīng)的value即為host值。(比如:若是你需要通過HTTPS訪問CDN節(jié)點資源,而CDN的節(jié)點往往服務(wù)了多個域名,所以需要通過SNI指定具體的域名證書進行通信。)
四、證書校驗
? ? ? ? 在證書校驗的過程中解決了SNI的問題之后呢,服務(wù)端會返回給客戶端一個與請求域名相匹配的證書,在客戶端驗證的時候若是默認取的請求地址中的host,而此時默認host早被我們換成了ip地址了,與證書是不匹配的,所以我們需要將header中的host取出進行證書驗證。數(shù)據(jù)處理過程如圖9 ;證書驗證過程如圖 10,一般SecTrustResultType的值為kSecTrustResultProceed或kSecTrustResultUnspecified表明驗證通過。


下面主要是一些讀取數(shù)據(jù)過程中對數(shù)據(jù)的處理,細節(jié)就不多說了,基本都是更CFNetwork相關(guān)部分。 如圖 11
