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 事件可以跟踪浏览器关闭前执行操作。
在浏览器的 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 来进行解决。
下面类似的实现框架可以用来跟踪 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 技术拦截
针对对象方法调用的拦截,比较成熟的方案是使用 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 的页面分析成节点如下的一颗树:
节点 { 父节点 ; 属性个数 ; 属性集合 ; 子节点个数 ; 子节点集合 } |
图 2 是个简单的 HTML 文本分析状态图,不支持 HTML 的 & 字符串拼接功能。可以反复调用这个模块用来从 HTML 文档中提取字符块生成相应的节点,然后可以利用 JavaScript 的正则表达时提取节点的属性。
通过 HTML 文本分析状态图可以得到 HTML 解析的代码,然后得到一个根为 root 的节点。后面对树进行进一步的处理,就可以实现很多拦截功能。比如 function 覆盖。前面讲到的用户自定义函数覆盖会受到代码插入点复杂的影响。如果在这种方法的拦截下,就可以实现分析出 <script> 节点的内容中含有特定的 function 定义,进而替换或者在其后插入新的函数定义,不会造成插入点复杂的结果