异步 JS拦截技术

JS拦截技术

HTTP 请求的拦截技术能够普遍地应用在反向代理、拦截 Ajax 通讯、网页的在线翻译、网站改版重构等方面。而拦截根据位置能够分为服务器端和客户端两大类,客户端拦截借助 JavaScript 脚本技术能够方便地和浏览器的解释器及用户的操做进行交互,可以实现一些服务器端拦截不容易实现的功能。本文将重点介绍经过 JavaScript 脚本在客户端对页面内容进行拦截修改的一些原理和技术。


在浏览器端的拦截和跟踪主要是利用 JavaScript 的脚本环境完成,根据笔者的经验,主要寻找并总结了以下的方法。这些方法的使用效果和支持平台能够互相弥补。浏览器

名称 特色 优势 缺点
利用浏览器的 Event 经过对 [ 鼠标事件 ],[ 键盘事件 ],[HTML 事件 ],[Mutation 事件 ] 的监听,能够对用户的交互,页面的变化,特别是标签节点的变化作出响应 浏览器自身支持,代码量小,几乎能够用来控制全部的 HTML 内容 此方法中的 Mutation Event,Firefox2.0 平台已支持,IE6.0 还没有支持
经过 AOP 技术拦截 能够拦截大部分对象的方法调用。 不少 JS 代码库和框架已经支持 AOP 技术,代码简单 ActiveX 对象没法有效拦截。没法拦截普通的函数。另外单独使用此项技术会形成插入点复杂。
覆盖函数进行拦截 经过编写同名方法覆盖系统定义,用户自定义的函数 ( 构造函数 ),达到拦截目的,对普通函数的拦截是对 AOP 的补充。 不依赖其余的代码库和 JS 框架,对系统函数的覆盖有很好的效果,能够拦截构造函数用来控制对象的生成。 拦截自定义函数会形成插入点复杂
经过动态代理进行拦截 主要用来解决 ActiveX 对象的拦截问题,经过构造 ActiveX 对象的代理对象,实现拦截和跟踪。 典型的例子如 IE 平台下 AJAX 通讯的拦截 代码复杂,属性更新的同步机制可能致使某些应用异常。
经过自代理和 HTML 解析进行拦截 此方法主要解决的是拦截时机的问题,配合上面的方法,就能够实现不少功能,而没必要要等待页面的 onload 事件。 实现浏览器端页面加载前拦截的好方法 代码复杂

利用浏览器的 Event 服务器

浏览器的事件也能够很好地用来拦截和跟踪页面。 鼠标和键盘的交互事件这里暂不介绍。比较经常使用的是 onload,onunload 事件和 Mutation Events 事件。 onload 和 onunload 事件能够做用在 window,frame,img 和 object 等对象上,利用 onload 能够在对象载入前执行一些操做 ,onunload 事件能够跟踪浏览器关闭前执行操做。app

在浏览器的 Event 中,Mutation Eventsii 是更加剧要的跟踪工具之一。 Mutation Events 是 DOM2.0 标准的一部分,目前 Firefox2.x 已经开始支持 Mutation Events, IE6.0 目前尚不支持。 在 IE6.0 中能够是经过使用 onpropertychange 事件及覆盖节点的方法弥补部分的不足。这里重点介绍一下 Mutation Events 。框架

Mutation Event 主要包括了七种事件,以下所示。 异步

  • DOMAttrModified:跟踪 DOM 节点属性的变化;
  • DOMCharacterDataModified:DOM 节点字符数据的变化;
  • DOMNodeInserted:DOM 新节点被插入到给定的父节点;
  • DOMNodeInsertedIntoDocument:DOM 节点被直接或随着祖先节点而插入;
  • DOMNodeRemoved:DOM 节点被从父节点删除;
  • DOMNodeRemovedFromDocument:DOM 节点被直接或跟随祖先节点被删除;
  • DOMSubtreeModified:DOM 元素或文档变化。

能够说利用 Onload 事件的拦截,咱们基本上解决了静态 HTML 内容的拦截,而对于脚本操做的 HTML 变化,咱们就能够经过 Mutation Event 来进行解决。async

下面相似的实现框架能够用来跟踪 src、action、href 等属性的变化。 函数

document.addEventListener("DOMAttrModified", AttributeNodeModified, false); function AttributeNodeModified(evt) { if(evt.attrName == "href") { } if(evt.attrName == "src") { } if(evt.attrName == "action") { } }工具

经过 DOMAttrModified、DOMNodeInserted 等事件能够很好地跟踪和拦截 HTML 的变化。只惋惜在 IE6.0 平台上还不能支持 Mutation Event,致使在 IE6.0 上进行拦截和跟踪的效果大打折扣。针对此平台,经过覆盖 document.write/DomNode.appendChild/DomNode.insertBefore 等方法和利用 onpropertychange 事件能够有限度地支持拦截和跟踪 HTML。网站

经过 AOP 技术拦截 this

针对对象方法调用的拦截,比较成熟的方案是使用 JavaScript 平台下的 AOP 技术。

目前,JavaScript 平台上的 AOP 方案主要有 Ajaxpectiii、jQuery AOPiv、Dojo AOPv 等。这些代码库主要功能是给指定对象的指定方法添加 Before, After,Around 等通知,从而达到拦截对象方法调用的目的 , 而且支持正则搜索方法名称。

Ajaxpect 的示例代码以下 :

var thing = { makeGreeting: function(text) { return 'Hello ' + text + '!'; }}function aopizeAdvice(args) { args[0] = 'AOP ' + args[0];return args;}function shoutAdvice(result) { return result.toUpperCase();} Ajaxpect.addBefore(thing, 'makeGreeting', aopizeAdvice); Ajaxpect.addAfter(thing, /make*/, shoutAdvice);

固然,在不方便使用上述代码库而且需求简单的时候,咱们一样能够经过对象的方法覆盖的方式达到一样的效果。可是不管 AOP 仍是方法覆盖, 都存在一个问题, 就是拦截代码的插入点不能作到很简捷,由于拦截代码的存在位置直接影响了代码的执行效果,所以在使用上还有必定的不方便。另外,针对 IE 平台的 ActiveX 对象,代码库不能很好的发挥功效,这是一些不足的地方。

覆盖系统类 / 方法进行拦截

覆盖已定义的函数是一种比 AOP 更直接的拦截和跟踪脚本调用的方式。

其原理是在原函数定义后,调用前经过定义同名函数,达到拦截和跟踪的目的。其通常形式多以下面 :

1: var oriFunction = someFunction; 2: someFunction = function () {3: return oriFunction(); //or oriFunction.call(x,);4: }

第一步是(第一行代码)为了将指向原来函数的指针保存,以便后续使用。

第二步即是定义同名函数,在同名函数里面的适当位置调用原来的功能。这种方法不但能够跟踪原来函数,还能够修改和过滤函数的参数,甚至能够修改返回值。当须要操纵参数的时候,只需在新定义的函数中访问 arguments 对象便可。

例如:针对系统函数 window.open(URL,name,specs,replace) 咱们能够经过下面的代码进行拦截:

var oriWindowOpen = window.open; window.open = function(url,names,specs,replace) { url = "http://www.ibm.com"; //or arguments[0]="http://www.ibm.com"; return oriWindowOpen(url,names,specs,replace); }

上面的拦截会致使全部的 window.open 调用所有打开 http://www.ibm.com 窗口 。

函数覆盖的适用范围较广,不但能够模拟 AOP 的实现,还能够对非对象函数进行操做。函数覆盖能够根据使用的差别分红若干状况 :

  • 覆盖系统定义的函数、对象的方法:覆盖系统定义的函数或方法能够不用顾及代码插入点的问题,大能够将函数覆盖的代码放置在页面的最前边,并参照上面的形式进行操做。可是特别注意在 IE 平台下对 ActiveX 的对象的方法无效。
  • 覆盖用户自定义的函数、对象的方法:覆盖用户自定义的函数,对象的方法须要考虑代码插入点的问题。正确的代码插入点的位置应该是在原函数定义以后,调用以前。
  • 覆盖构造函数:覆盖构造函数是知足上面两种状况的一种特殊使用形式,跟踪对象建立之除,能够有效地针对对象的须要做出各类特殊的设置。

    覆盖构造函数的通常形式 :

    var oriFunction = someFunction;someFunction = function () { temp = oriFunction(); //oriFunction.call(x,); return temp;}

下面结合动态代理的方法给出 IE/Firefox 平台的 Ajax 通讯拦截的一种简单实现。

Ajax 通讯的核心是经过 XMLHttpRequest 对象和 HTTP Server 进行通讯 ( 同步 / 异步 ),Firefox 和 IE 平台对 XMLHttpRequest 对象的实现不同,所以两种浏览器的拦截方案也截然不同。咱们经过上面的技术将对 XMLHttpRequest 对象的方法进行跟踪。

拦截方法调用,咱们可使用 AOP,固然也能够直接覆盖函数。

在 Firefox 平台,咱们能够经过下面的代码实现拦截 Ajax 对象的通讯:

var oriXOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method,url,asncFlag,user,password) { //code to trace or intercept oriXOpen.call(this,method,url,asncFlag,user,password); };

可是在 IE 6.0 平台,上面的代码将不会有做用,由于在 IE 6.0 平台,Ajax 通讯对象是经过 ActiveX 对象完成的,JS 中的函数覆盖不能起到做用。

经过动态代理进行拦截

当在 IE6.0 平台遭遇 ActiveX 对象的时候,面对直接的函数覆盖不能奏效的时候,咱们能够考虑经过另一种办法,即动态代理 ActiveX 对象的方式实现拦截和跟踪。

首先咱们经过覆盖构造函数的方法,将建立 XMLHttpRequest 对象的过程进行改造。

var oriActiveXObject = ActiveXObject; ActiveXObject = function(param) { var obj = new oriActiveXObject(param); if(param == "Microsoft.XMLHTTP" || param=="Msxml2.XMLHTTP" || param == "Msxml2.XMLHTTP.4.0") { return createActiveXAgent(obj); } return obj; };

咱们将构造过程拦截下来后,进行本身的改造,主要操做是建立对象,对象中设置与 ActiveX 对象相同的属性和方法,而且还须要同步属性方法。

function createActiveXAgent(ao) { var agent = new Object; agent.activeXObject = ao; //被包裹的内核,是真正的通讯对象 agent.syncAttribute = function() { //syncAttribute是用来同步属性的 try{ this.readyState = this.activeXObject.readystate; this.responseText = this.activeXObject.responseText; this.responseXML = this.activeXObject.responseXML; this.status = this.activeXObject.status; this.statusText = this.activeXObject.statusText; }catch(e) { } }; agent.trigStateChange = function() { //模拟onreadystatechange agent.syncAttribute(); if(agent.onreadystatechange != null) { agent.onreadystatechange(); } }; agent.activeXObject.onreadystatechange = agent.trigStateChange; agent.abort = function() { //模拟abort this.activeXObject.abort(); this.syncAttribute(); }; agent.getAllResponseHeaders =function() { //模拟内核对应的方法 var result = this.activeXObject.getAllResponseHeaders(); this.syncAttribute(); return result; }; agent.getResponseHeader = function(headerLabel) { //模拟内核对应的方法 var result = this.activeXObject.getResponseHeader(headerLabel); this.syncAttribute(); return result; }; agent.open = function(method,url,asyncFlag,userName,password) { //code to trace and intercept; this.activeXObject.open(method,url,asyncFlag,userName,password); this.syncAttribute(); }; agent.send = function(content) { //模拟内核对应的方法 this.activeXObject.send(content); this.syncAttribute(); }; agent.setRequestHeader = function (label,value) { //模拟内核对应的方法 this.activeXObject.setRequestHeader(label,value); this.syncAttribute(); }; return agent;};

从上面的代码能够看出来,代理对象经过自身的方法模拟了原来 ActiveX 对象的方法。而更关键的属性问题,是经过在函数调用先后的属性同步函数实现的。即:在调用代理内核方法以前,将属性从代理对象同步给内核对象;在内核方法调用以后,将属性从内核对象同步给代理对象。

由于 AJAX 对象的属性几乎不被用户写入,故上面的实现只须要单向属性同步,即将内核属性同步给代理属性。对于复杂的应用,能够经过双向属性同步函数来解决属性的代理问题。

这种动态代理的方法将 ActiveX 对象像果核同样包裹起来,经过代理对象自身的同名属性和方法提供给外界进行访问,从而达到跟踪和拦截的目的。

经过自代理和 HTML 解析进行拦截

当代码拦截点须要简单可靠的时候,上面的方法没法很好的知足需求。因而咱们须要新的思路来解决代码的拦截点问题。

自代理和 HTML 解析是经过拦截原有的 HTTP 通讯,经过从新通讯得到内容后,在浏览器解析前经过咱们本身的代码进行简单解析过滤的方式进行代码处理的方案。

首先是拦截原有的解析,从新加载新的内容:

var s = document.location.href;var comm = new ActiveXObject("Microsoft.XMLHTTP"); comm.open('get',s,false); comm.onreadystatechange = function() { if(comm.readyState == 4) { document.execCommand("stop"); var retText = removeMe(comm.responseText); retText = processContent(retText); } } comm.send(null);

若是将上面的代码写在一个 js 文件里,而后经过 <script> 标签插入到页面的最开始位置(<HTML> 后面)。

在 IE6.0 的浏览器下面,上面的代码由于使用了 XMLHTTP 的同步通讯机制,所以代码会阻塞在 comm.send(null) 处,当通讯结束获得完整的页面以后,会触发 stop 致使浏览器中止解析,转而执行咱们的 processContent. removeMe 的意义在于从新得到的片面中去除这段代码自身,防止无穷迭代。

在 Firefox 下,咱们须要使用 window.stop 代替上面的 execCommand.

当咱们抢在浏览器以前拿到 HTML 内容后,咱们下面的任务就是分析 HTML. 目前尚没有成熟的 JS 分析 HTML 的框架。所以咱们能够选择将 HTML 转换成 XML, 而后借助 DOM 进行分析,也能够实现咱们本身的 HTML 分析方案 .

咱们能够将 HTML 的页面分析成节点以下的一颗树:

节点 { 父节点 ; 属性个数 ; 属性集合 ; 子节点个数 ; 子节点集合 }

JS拦截技术 - yong1202 - 蚂蚁的博客

图 2 是个简单的 HTML 文本分析状态图,不支持 HTML 的 & 字符串拼接功能。能够反复调用这个模块用来从 HTML 文档中提取字符块生成相应的节点,而后能够利用 JavaScript 的正则表达时提取节点的属性。

经过 HTML 文本分析状态图能够获得 HTML 解析的代码,而后获得一个根为 root 的节点。后面对树进行进一步的处理,就能够实现不少拦截功能。好比 function 覆盖。前面讲到的用户自定义函数覆盖会受到代码插入点复杂的影响。若是在这种方法的拦截下,就能够实现分析出 <script> 节点的内容中含有特定的 function 定义,进而替换或者在其后插入新的函数定义,不会形成插入点复杂的结果