網絡劫持通常有兩種狀況,一種是DNS劫持
,另外一種是HTTP劫持
。css
從表現上區分這兩種劫持很是簡單。html
若是是DNS劫持
,你輸入的網址是google.com,而後出來的頁面是百度。前端
若是是HTTP劫持
,你打開了google.com,但是右下角彈出了百度推廣的不孕不育廣告。c++
URL域名解析成ip地址的過程被稱做 DNS 解析
。在這個過程當中,因爲 DNS 請求報文是明文狀態,可能會在請求過程當中被監測,而後攻擊者假裝DNS服務器向主機發送帶有假ip地址的響應報文,從而使得主機訪問到假的服務器。這個就是DNS劫持的根本原理。web
而另外一種就是HTTP劫持
。在運營商的路由器節點上,設置協議檢測,一旦發現是HTTP請求,並且是html類型請求,則攔截處理。後續作法每每分爲2種,1種是相似DNS劫持
返回302讓用戶瀏覽器跳轉到另外的地址,還有1種是在服務器返回的 HTML 數據中插入 js 或 dom 節點,從而使網頁中出現本身的廣告等等垃圾信息。objective-c
通常來講,針對各類網絡劫持,大部分工做都是由前端來完成,針對這一方面的研究,也大多都是前端開發方向。可是其實客戶端也能夠經過一些方法來防劫持。segmentfault
做爲客戶端開發,咱們應該先了解咱們的URL Loading System。瀏覽器
雖然 URL 加載系統包含的內容衆多,但代碼的設計上卻很是良好,沒有把複雜的操做暴露出來,開發者只須要在用到的時候進行設置。(蘋果官方文檔About the URL Loading System,是每一個 iOS 開發者都應該認真研究的。)緩存
DNS劫持
的問題,就能夠基於 NSURLProtocol
實現 LocalDNS
防劫持方案。安全
關於LocalDNS
防劫持方案,能夠參考一篇大神文章DNS防劫持。
簡單來講,在網頁發起請求的時候獲取請求域名,而後在本地進行解析獲得ip
,返回一個直接訪問網頁ip
地址的請求。
結構體struct hostent
用來表示地址信息:
struct hostent { char *h_name; // official name of host char **h_aliases; // alias list int h_addrtype; // host address type——AF_INET || AF_INET6 int h_length; // length of address char **h_addr_list; // list of addresses };
經過C函數gethostbyname
,使用遞歸查詢的方式將傳入的域名轉換成struct hostent
結構體,在本地將URL
解析成123.123.25.53
這種ip
地址。具體實現參考文章中的代碼。
另外還能夠用從服務器下發對應的DNS
解析列表來代替遞歸查詢這種比較低效的方式,文章中也有介紹。
而不管是那種方式,NSURLProtocol
都是處理的核心部分。
NSURLProtocol
或許是 URL 加載系統中最功能強大但同時也是最晦澀的部分了。它是一個抽象類,你能夠經過子類化來定義新的或已經存在的 URL 加載行爲。
用了它,你沒必要改動應用在網絡調用上的其餘部分,就能夠改變URL加載行爲的所有細節。 NSURLProtocol
就是一個蘋果容許的中間人攻擊。
下面這麼多需求,均可以經過 NSURLProtocol
,在不改動其餘代碼的狀況下,比較簡單地就能實現:
攔截圖片加載請求,轉爲從本地文件加載
爲了測試對 HTTP 返回內容進行mock和stub
對發出請求的header進行格式化
對發出的媒體請求進行簽名
建立本地代理服務,用於數據變化時對URL請求的更改
故意製造畸形或非法返回數據來測試程序的魯棒性
過濾請求和返回中的敏感信息
在既有協議基礎上完成對 NSURLConnection 的實現且與原邏輯不產生矛盾
官方文檔對 NSURLProtocol
的描述是這樣的:
An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports.
在每個 HTTP 請求開始時,URL 加載系統建立一個合適的 NSURLProtocol
對象處理對應的 URL 請求,而咱們須要作的就是寫一個繼承自 NSURLProtocol
的類,並經過 - registerClass:
方法註冊咱們的協議類,而後 URL 加載系統就會在請求發出時使用咱們建立的協議對象對該請求進行處理。
如何使用 NSURLProtocol 攔截 HTTP 請求?有這個麼幾個問題須要去解決:
如何決定哪些請求須要當前協議對象處理?
對當前的請求對象須要進行哪些處理?
NSURLProtocol
如何實例化?
如何發出 HTTP 請求而且將響應傳遞給調用者?
這幾個問題其實均可以經過 NSURLProtocol
爲咱們提供的 API 來解決,決定請求是否須要當前協議對象處理的方法是:+ canInitWithRequest
,每一次請求都會有一個 NSURLRequest
實例,上述方法會拿到全部的請求對象,咱們就能夠根據對應的請求選擇是否處理該對象。
那麼到底是否要處理對應的請求。因爲網頁存在動態連接的可能性,簡單的返回YES
可能會建立大量的NSURLProtocol
對象,所以咱們須要保證每一個請求能且僅能被返回一次YES
。
請求通過 + canInitWithRequest:
方法過濾以後,咱們獲得了全部要處理的請求,接下來須要對請求進行必定的操做,而這都會在 + canonicalRequestForRequest:
中進行,雖然它與 + canInitWithRequest:
方法傳入的 request 對象都是一個,可是最好不要在 + canInitWithRequest:
中操做對象,可能會有語義上的問題。
因此,咱們須要覆寫 + canonicalRequestForRequest:
方法提供一個標準的請求對象:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; }
這裏就能夠對request作一些修改,好比加個header什麼的,只須要最後能返回一個NSURLRequest便可。
若是處理請求返回了YES
,那麼下面兩個回調對應請求開始和結束階段。在這裏能夠標記請求對象已經被處理過。
- (void)startLoading; - (void)stopLoading;
在以前的 iOS 客戶端基於 WebP 圖片格式的流量優化 這篇文章中,就是利用- (void)startLoading;
來替換圖片請求的。當時替換 WebP 圖片的核心功能,實際上就是在NSURLProtocol
中完成的。
實際上,這種劫持對於大部分客戶端來講,是無能爲力的,須要前端來處理。不過,有些特殊狀況下,客戶端也能夠有一些針對 HTTP劫持
的辦法。
好比像媒體新聞類客戶端的文章中,防止js注入
。
在具體的實現方式以前,須要有一些準備工做,就是關於URL Loading System
中的NSURLCache
。
NSURLCache
爲您的應用的 URL 請求提供了內存中以及磁盤上的綜合緩存機制。 做爲基礎類庫 URL Loading System 的一部分,任何經過 NSURLConnection
加載的請求都將被 NSURLCache
處理。
當一個請求完成獲得來自服務器的Response
,在本地保存做爲cache。下一次同一個請求再發起時,本地保存的Response
就會立刻返回,不須要鏈接服務器。NSURLCache
會 自動 且 透明 地返回迴應。
在NSURLConnection加載系統中,緩存被設計爲request對象的一個屬性,由NSURLRequest對象的cachePolicy屬性指定。而在NSURLSession加載系統中,緩存被設計爲 NSURLSessionConfiguration對像的一個屬性,該屬性所指定的策略被該session的全部request所共享。
做爲一個Cache
,它頭文件中提供的方法並不複雜,就是基本的增刪查改,(其中增和改能夠算是一個功能,沒有就增,改就是覆蓋)。主要方法僅六個:
// 初始化方法 - (instancetype)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(nullable NSString *)path; // 查詢方法 - (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request; // 存儲方法 - (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request; // 刪除方法 - (void)removeCachedResponseForRequest:(NSURLRequest *)request; - (void)removeAllCachedResponses; - (void)removeCachedResponsesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);
很是簡潔的類,功能高度封裝,用起來很簡單。可是它也開闢了一個新世界,就是你能夠實現一個子類,來接管系統的URLCache
功能。只須要一個簡單的步驟:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { STURLCache *URLCache = [[STURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil]; [NSURLCache setSharedURLCache:URLCache]; }
這樣,STURLCache
就能夠接管緩存的管理了。當系統調用URLCache
的增刪改查方法時,均可以由子類來接管。
NSURLCache
的緩存策略,以及和HTTP header
之間的關係,能夠參考NSHipster NSURLCache文章,再也不深刻。
這樣,配合NSURLProtocol
,就能夠對緩存作精準控制了。
這個方法,適用於新聞類app,由於新聞類app的web頁都是本身寫的,因此,js和css都是可知的。
防注入的方式就是這樣:
發版前,在 bundle 中存一份最新的前端js文件
後臺在返回 js 文件 URL 的時候,對 js 文件內容進行 SHA-256
,獲得的 hash 值拼接到 js 的文件名中
請求 js 資源文件時,在NSURLCache
中的- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
方法中攔截請求
攔截請求以後,判斷本地是否有緩存,若是有,則直接返回緩存文件包裝成 response
下載 js 資源時,走NSURLProtocol
代理方法- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
,對 data 進行SHA-256
簽名比對,若是簽名一致,將 data 經過;若是簽名不一致,表明 js 被污染,直接丟棄,從bundle取出本地預存的 js 文件返回回來。
代碼自己不會有什麼難處,因此就只寫出基本邏輯。
在自定義的URLCache實現文件中:
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request { NSString* ua = [request valueForHTTPHeaderField:@"User-Agent"]; if (!EmptyString(ua) && [ua lf_containsSubString:@"AppleWebKit"]) { if ([CacheManager shouldVerifyHashCode:request]) { //包含64位hashcode的js css文件 // 取本地JS緩存 NSData *resultData = [CacheManager getJSCache]; if (resultData) { NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:nil expectedContentLength:resultData.length textEncodingName:nil]; return [[NSCachedURLResponse alloc] initWithResponse:response data:resultData]; } else { return nil; } } return [super cachedResponseForRequest:request]; } - (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request { NSString* ua = [request valueForHTTPHeaderField:@"User-Agent"]; if ([CacheManager shouldVerifyHashCode:request] && [ua lf_containsSubString:@"AppleWebKit"]) { // 將請求回來的,而且經過驗證的新js放到緩存中 [[CacheManager defaultManager] storeCachedResponse:cachedResponse forRequest:request]) return; } [super storeCachedResponse:cachedResponse forRequest:request]; }
而在自定義的NSURLProtocol
子類中
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { NSString *ua = [request valueForHTTPHeaderField:@"User-Agent"]; if ([CacheManager shouldVerifyHashCode:request] && [ua lf_containsSubString:@"AppleWebKit"]) { // 攔截js請求 return YES; } return NO; } // 收到請求返回data的代理方法 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { if([data verifySHA256Success]) { [self.client URLProtocol:self didLoadData:data]; } else { localData = [CacheManager bundleCacheFromUrl:url]; [self.client URLProtocol:self didLoadData:localData]; } }
比較抽象的一點就是,這個方案是否是不能再更新 js 了?
固然不會,由於當後臺的 js 文件有更新時,新 js 文件的簽名就會發生變化,js 文件的URL也就天然變化,因而本地請求的時候,緩存是沒法命中的,因此,也就會直接走下載 js 的那個路徑。
這種方案的缺點就是:
在發生 js 劫持的時候,只能使用本地 js,可能會比最新版本 js 落後
js 文件必須是由本身的服務端提供,並控制,纔好對 js 進行簽名,因此適用範圍略窄
做爲這個方案的擴充,能夠考慮再次利用NSURLProtocol
,當發現 js 被污染,重定向URL,此URL由服務端返回一個加密的 js 文件,對稱加密,密鑰插入在 js 的密文中,本地解密 js 文件,就能夠保證獲得最新的,安全的 js 文件了。
不過話說回來,既然都這麼費勁的話,爲啥不讓前端來幫忙作呢,或者直接上HTTPS
,纔是真正的防劫持之道。
關於運營商網絡劫持,我本人畢竟不是前端開發,因此有不少問題能夠理解也不是很準確,確定也不是太專業。只是提供一個比較少看到的功能實現。其實防劫持最終的解決辦法就是HTTPS,不過咱們仍是能夠經過這方面的思索,來嘗試一些更深刻,更好玩的東西。並且能夠更深刻地去探索蘋果框架下的URL Loading System。
iOS 開發中使用 NSURLProtocol 攔截 HTTP 請求
更多其餘文章歡迎訪問個人博客 http://suntao.me