移動端H5防劫持(防止廣告注入)
移動端H5防劫持(防止廣告注入)
最近專案中自己的H5網頁出現了被劫持插入廣告的事件,看好趁著這個節點整理下H5被劫持的原因及防止劫持的方法。
原因:
經過查詢和調研市面上出現這種情況的原因大概分為三種:
1.DNS劫持(也就是運營商搞的鬼)
2.http劫持(此類情況最多)
3.專案中使用第三方的jar包
首先定位我們自己出現此類問題應該從以下幾個方面查詢、除錯:
1.網路用的是4g還是wifi?
2.如果是wifi,是不是路邊免費的wifi?
3.在什麼頁面出現?(方便定位是h5頁面還是原生介面,對大部分使用者,千萬不要問他是h5還是原生,只能自己分析,如果原生介面也出現那麼第三方jar出現問題的概率很大,如果只是h5介面,那麼劫持的可能性很大)
4.使用的是android還是ios客戶端
什麼是DNS劫持:
首先DNS是什麼。在因特網中,機器相互識別靠的是ip,而ip單純的無意義數字的結合,很難被人類熟記,所以產生了域名,例如 ofollow,noindex">www.baidu.com 就是域名。那麼問題來了,我們輸入域名,機器又不認識,那麼機器怎麼去訪問?那麼就輪到DNS出場了,DNS在作為域名和IP地址相互對映的一個分散式資料庫,就是我們的瀏覽器,會將域名拿到DNS去解析出ip地址來訪問,DNS劫持是指在劫持的網路範圍內攔截域名解析的請求,分析請求的域名,把審查範圍以外的請求放行,否則返回假的IP地址或者什麼都不做使請求失去響應,其效果就是對特定的網路不能反應或訪問的是假網址。
通俗來說,就是他給我們指向了另一個地址,或者讓我們無法訪問。電信以前的互聯星空的,每次聯網開啟的第一個網頁永遠是互聯星空。就是典型的DNS劫持。
什麼是http劫持:
百度百科的說法:HTTP劫持是在使用者與其目的網路服務所建立的專用資料通道中,監視特定資料資訊,提示當滿足設定的條件時,就會在正常的資料流中插入精心設計的網路資料報文,目的是讓使用者端程式解釋“錯誤”的資料,並以彈出新視窗的形式在使用者介面展示宣傳性廣告或者直接顯示某網站的內容。
通俗來說,你要去百度的首頁,他會給你百度首頁,然後再百度首頁的某個部位+個廣告。 更具欺騙性,危害更大。
解決方案:
1.使用https請求替換http請求,可以有效的防止劫持。原理是:因為SSl協議唉http請求開始前增加了握手階段:

https.png
在SSL握手階段,客戶端瀏覽器會認證伺服器的身份,這是通過“證書”來實現的,證書由證書權威(CA)為某個域名簽發,可以理解為網站的身份證件,客戶端需要對這個證件進行認證,需要確定該證書是否屬於目標網站並確認證書本身是否有效。最後在握手階段,通訊的雙方還會協商出一個用於加密和解密的會話金鑰。
SSL握手階段結束之後,伺服器和客戶端使用協商出的會話金鑰對互動的資料進行加密/解密操作,對於HTTP協議來說,就是將HTTP請求和應答經過加密之後再發送到網路上。
移動端處理:
1.移動端攔截協議、只攔截自己的載入本地的js相關。NSURLProtocol->、SystemWebViewClient -> shouldInterceptRequest
2.可以建立黑名單、服務端下發、實時動態更新黑名單、手機端程式碼攔截名單中的域名或者請求
android程式碼處理:
webView.setWebViewClient(new WebViewClient() { // Load opened URL in the application instead of standard browser // application public boolean shouldOverrideUrlLoading(WebView view, String url) { showLogInfo("攔截到的url----"+url); String advertising="http://"+sharedPreferencesUtil.getData(Constant.IP, RequestConfig.IP) +":"+sharedPreferencesUtil.getData(Constant.PORT,RequestConfig.IPPORT); if (url.contains(pre)) { Map<String, String> map = getParamsMap(url, pre); String code = map.get("code"); String data = map.get("data"); parseCode(code, data); return true; }else if(!url.contains(advertising)){ showLogError("攔截到植入廣告,廣告的url——"+url); return true; } else{ return false; } } });
iOS處理程式碼:
1.在WebView代理裡攔截
2.全域性攔截請求
#import "ZMURLProtocol.h" static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey"; static NSDictionary *_holdUpDic; @interface ZMURLProtocol ()<NSURLConnectionDelegate> @property (nonatomic, strong) NSURLConnection *connection; @end @implementation ZMURLProtocol +(NSDictionary *)getHoldUpDic { if (!_holdUpDic) { #pragma mark - 這裡是獲取黑白名單的資料 /* [AFNetWork postWithURL:@"" Params:@"" Success:^(NSURLSessionDataTask *task, id responseObject) { //獲取廣告攔截資料 _holdUpDic = responseObject; //寫入本地plist檔案 BOOL success = [_holdUpDic writeToFile:path atomically:YES]; if (success ) { NSLog(@"寫入成功"); }else { NSLog(@"寫入失敗"); } }]; _holdUpDic = [NSDictionary dictionaryWithContentsOfFile:path]; */ } return _holdUpDic; } + (BOOL)canInitWithRequest:(NSURLRequest *)request { //只處理http和https請求 NSString *scheme = [[request URL] scheme]; if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame )||([scheme caseInsensitiveCompare:@"https"] == NSOrderedSame )) { //看看是否已經處理過了,防止無限迴圈 if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) { return NO; } return YES;//處理 } return NO; } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request { //網頁發生變動 [[NSNotificationCenter defaultCenter] postNotificationName:PageChangeNotification object:self userInfo:nil]; // NSLog(@"canonicalRequestForRequest:%@",request.URL.absoluteString); NSMutableURLRequest *mutableReqeust = [request mutableCopy]; mutableReqeust = [self redirectHostInRequset:mutableReqeust]; return mutableReqeust; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } - (void)startLoading { NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy]; //打標籤,防止無限迴圈 [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust]; // self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self]; } // - (void)stopLoading { [self.connection cancel]; } #pragma mark - NSURLConnectionDelegate - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; } - (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response { // if (response != nil) // { //[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response]; // } //這裡需要回傳[self client] 訊息,那麼需要重定向的網頁就會出現問題:host不對或者造成跨域呼叫導致資源無法載入 [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response]; // return request; // 這裡如果返回 request 會重新請求一次 return nil; } - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection { return YES; } - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { [self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge]; } - (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { [self.client URLProtocol:self didCancelAuthenticationChallenge:challenge]; } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; } - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { return cachedResponse; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; } #pragma mark -- private +(NSMutableURLRequest*)redirectHostInRequset:(NSMutableURLRequest*)request { //沒有域名的URL請求就原路返回,不能返回nil ,不然在跳轉APP的時候會被攔截返回空出錯(或者其他情況). //eg: mqq://im/chat?chat_type=wpa&uin=1299101858&version=1&src_type=web 跳轉到指定QQ使用者的聊天視窗 if ([request.URL host].length == 0) { return request; } NSString *originUrlString = request.URL.absoluteString; //獲取主機名字,在這裡執行正則匹配 NSString *originHostString = [request.URL host]; NSRange hostRange = [originUrlString rangeOfString:originHostString]; //找不到主機名,返回 if (hostRange.location == NSNotFound) { return request; } if (originUrlString != nil) { //獲取攔截的黑白名單資料(過濾名單) //這個是自定義方法,你們自己隨意發揮,哈哈哈. #warning --- 思路實現 /* 這裡的匹配黑白名單一般只是**匹配域名** 思路 1:匹配白名單->匹配黑名單-> 如果兩個都沒有,就向伺服器列印日誌. (拉外網) 思路 2:匹配白名單 以下程式碼運用思路1 實現 eg: 這個是過濾的規則的例子格式 .*(.qq.com|api.weibo.com|.weibo.com|.baidu.com|.weixin.qq.com|.sina.com|.sina.cn).* */ NSDictionary *dic = [self getHoldUpDic]; if (!dic) //如果為空不處理黑白名單 { return request; } //白名單 NSString *whiteList = dic[@"whiteList"]; //黑名單 NSString * blackList = dic[@"blackList"]; #pragma mark - 白名單匹配 //1.1將正則表示式設定為OC規則 if (![whiteList isEqualToString:@""]) { NSRegularExpression *regular1 = [[NSRegularExpression alloc] initWithPattern:whiteList options:NSRegularExpressionCaseInsensitive error:nil]; //2.利用規則測試字串獲取匹配結果 NSArray *results1 = [regular1 matchesInString:originUrlString options:0 range:NSMakeRange(0, originUrlString.length)]; if (results1.count > 0) //是白名單,允許訪問 { return request; } } #pragma mark - 黑名單匹配 if (![blackList isEqualToString:@""]) { //1.1將正則表示式設定為OC規則 NSRegularExpression *regular2 = [[NSRegularExpression alloc] initWithPattern:blackList options:NSRegularExpressionCaseInsensitive error:nil]; //2.利用規則匹配字串獲取匹配結果 NSArray *results2 = [regular2 matchesInString:originUrlString options:0 range:NSMakeRange(0, originUrlString.length)]; if (results2.count > 0 ) //黑名單,返回nil; { return request; } } if (![whiteList isEqualToString:@""]&&![blackList isEqualToString:@""]) { #pragma mark - 傳送到服務端列印日誌 } } return request; } @end
H5處理:
嵌入的程式碼基本都是iframe,把以下js程式碼加入 body標籤內,以刪除iframe(記得用script標籤包裹)
//以下程式碼為刪除嵌入廣告 var del_times = 0, deTimer = null; function adGo() { var iframe = document.getElementsByTagName('iframe')[0]; if(iframe){ console.log(iframe) //迴圈 iframe 父類,直到找到body和body的下一級,然後整個嵌入的程式碼刪除。 var bodyNode = {tagName:''}, iframeParent, targetNode = iframe.parentNode; while (bodyNode.tagName != 'BODY'){ bodyNode = targetNode; if(bodyNode.tagName != 'BODY'){ iframeParent = targetNode; targetNode = targetNode.parentNode; } } if(iframeParent) //如果iframe有父類 bodyNode.removeChild(iframeParent); else bodyNode.removeChild(iframe); } del_times++; if (del_times > 10) window.clearInterval(deTimer) } deTimer = self.setInterval(adGo, 1000);//把這個1000, 調低一點,比如200
以上就是處理方法,如有遺漏請大家指正。