移动端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单纯的无意义数字的结合,很难被人类熟记,所以产生了域名,例如www.baidu.com 就是域名。那么问题来了,我们输入域名,机器又不认识,那么机器怎么去访问?那么就轮到DNS出场了,DNS在作为域名和IP地址相互映射的一个分布式数据库,就是我们的浏览器,会将域名拿到DNS去解析出ip地址来访问,DNS劫持是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。
通俗来说,就是他给我们指向了另一个地址,或者让我们无法访问。电信以前的互联星空的,每次联网打开的第一个网页永远是互联星空。就是典型的DNS劫持。
什么是http劫持:
百度百科的说法:HTTP劫持是在使用者与其目的网络服务所建立的专用数据通道中,监视特定数据信息,提示当满足设定的条件时,就会在正常的数据流中插入精心设计的网络数据报文,目的是让用户端程序解释“错误”的数据,并以弹出新窗口的形式在使用者界面展示宣传性广告或者直接显示某网站的内容。
通俗来说,你要去百度的首页,他会给你百度首页,然后再百度首页的某个部位+个广告。 更具欺骗性,危害更大。
解决方案:
使用https请求替换http请求,可以有效的防止劫持。原理是:因为SSl协议唉http请求开始前增加了握手阶段:
在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
以上就是处理方法,如有遗漏请大家指正。