本文探讨用于构建实时(real-time)跨域(cross-origin)通信的两个重要模块:跨文档消息通讯和XMLHttpRequest Level 2。通过它们,我们可以构建引人注目的Web应用,作为HTML5应用新的通信手段,这两个构建块可以让不同域间的web应用安全地进行通信。
跨文档消息通信
处于安全方面考虑,运行在同一浏览器中的框架、标签页、窗口之间的通讯一直受到严格的限制。例如,在浏览器内部共享信息对某些站点可能比较方便,但是同时也增加了收到恶意攻击的可能性。如果浏览器允许程序访问加载到其他框架和标签的内容,某些网站就能够利用脚本窃取其他网站的某些信息。浏览器厂商合理第限制了这类访问,当尝试检测或修改从其他源加载的内容时,浏览器会抛出安全异常,并组织相应的操作。
然而,实际中存在一些合理的让不同站点的内容能在浏览器内进行交互的需求,如果浏览器内部提供直接的通信机制,就能更好地组织这些应用:跨文档消息通信。
跨文档消息通信可以确保iframe、标签页、窗口之间安全的进行跨源通信。PostMessage是Windows API(应用程序接口) 中的一个常用函数,用于将一条消息放入到消息队列中。该方法可以通过绑定window的message事件来监听发送跨文档消息传输内容。
在父网页中通过 iframe 嵌入子页面,并在 JavaScript 代码中调用 postMessage 方法发送数据到子窗口。
父页面中嵌入子页面,调用 postMessage 方法发送数据
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Test Cross-domain communication using HTML5</title> <script type="text/JavaScript"> function sendIt(){ // 通过 postMessage 向子窗口发送数据 document.getElementById("otherPage").contentWindow .postMessage( document.getElementById("message").value, "http://loverMap.sinaapp.com" ); } </script> </head> <body> <!-- 通过 iframe 嵌入子页面 --> <iframe src="http://loverMap.sinaapp.com/test/child.html" id="otherPage"></iframe> <br/><br/> <input type="text" id="message"><input type="button" value="Send to child.com" onclick="sendIt()" /> </body> </html>
在子窗口中监听 onmessage 事件,并用 JavaScript 实现显示父窗口发送过来的数据。
子窗口中监听 onmessage 事件,显示父窗口发送来的数据
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Web page from child.com</title> <script type="text/JavaScript"> //event 参数中有 data 属性,就是父窗口发送过来的数据 window.addEventListener("message", function( event ) { // 把父窗口发送过来的数据显示在子窗口中 document.getElementById("content").innerHTML+=event.data+"<br/>"; }, false ); </script> </head> <body> <div id="content"></div> </body> </html>
演示地址:http://lovermap.sinaapp.com/test/parent.html
在postMessage之前,iframe间的通信有时会通过直接写脚本来实现。通过执行一个页面中的脚本来尝试操纵另一个文件。这种方式,可能会因为安全限制而被进制,因而postMessage取代了直接编程访问,它提供了javascript环境中的异步通信机制。
假如没有postMessage,跨源通信导致安全错误,为防止跨站点(cross-site)脚本攻击,浏览器会强制引发这种安全限制。
Error:Permission denied for <http://localhost:8080>to get property window.document from <http://loverMap.sinaapp.com>
理解源安全
源是在网络上应用建立信任关系的地址的子集。源有规则(scheme)、主机(host)、端口(port)组成。
html5 定义源的序列化,源在API和协议中以字符串的形式出现,对于使用XMLHttpRequest进行跨域HTTP请求是非常重要,对WebSocket也一样。
在处理跨源通信的消息时,一定要验证每个消息的源。此外,处理消息同的数据时也应该谨慎。即使消息来自可信源,也应该像对待外部输入时一样仔细。
element.innerHTML = e.data,是危险的因为e.data,会被当作标记;而element.textContent = e.data则是相对安全的。最好不要用第三方的字符串求值,另外,避免使用eval方法处理应用内部的字符串,可以通过window.JSON或者json.org解析器使用JSON。
浏览器支持
if(typeof window.postMessage == "undefined"){ //浏览器不支持postMessage }
发送消息
window.postMessage( document.getElementById("message").value, "http://loverMap.sinaapp.com");
该方法使用两个参数:第一个参数为所发送的消息文本,但也可以是任何 JavaScript 对象(通过 JSON 转换对象为文本),第二个参数为接收消息的对象窗口的 URL 地址,可以在 URL 地址字符串中使用通配符'*'指定全部地。方法使用两个参数:第一个参数为所发送的消息文本,但也可以是任何 JavaScript 对象(通过 JSON 转换对象为文本),第二个参数为接收消息的对象窗口的 URL 地址,可以在 URL 地址字符串中使用通配符'*'指定全部地。
监听消息事件
脚本可以通过监听window对象中的事件来接受信息。
window.addEventListener("message", function( event ) { document.getElementById("content").innerHTML+=event.data+"<br/>"; }, false );
实例的应用的运行依赖于两个先决条件:首先,页面需要部署在web服务器上;其次,两个页面必须来自不同的域。如果可以访问位于不同的域的多个web服务器,那么将实例文件部署到服务器之后,实例就能顺利运行了。
XMLHttpRequest Level 2
XMLHttpRequest API使得AJAX技术的实现成为可能。作为XMLHttpRequest的改进版,,XMLHttpRequest Level 2在功能上有很大的改进。主要集中在两方面:
1)跨域XMLHttpRequest
2)进度事件。
传统的XMLHttpRequest
var xhr = new XMLHttpRequest(); xhr.open('GET', 'example.php'); xhr.send(); xhr.onreadystatechange = function(){ if ( xhr.readyState == 4 && xhr.status == 200 ) { console.info( xhr.responseText ); } else { console.info(xhr.statusText ); } };
包含XMLHttpRequest的主要属性
xhr.readyState:xhr对象的状态,等于4表示数据接受完毕。
xhr.status:服务器返回的状态码,等于200表示一切正常。
xhr.responseText:服务器返回的xml格式的数据。
xhr.statusText:服务器返回的状态文本。
但是传统的XMLHttpRequest具有一下缺点
1)只支持文本数据的传送,无法用来读取和上传二进制文件。
2)传送和接收数据时,没有进度信息,只能提示有没有完成。
3)受到同源限制,只能向同一域名的服务器请求数据。
跨域XMLHttpRequest
过去XMLHttpRequest仅限于同源通信。XMLHttpRequest Level 2通过CORS(跨源资源共享)实现XMLHttpRequests。由于路径可能包含敏感信息,为了保护用户隐私,浏览器不一定会发送Referer,而浏览器在任何必要的时候都会发送Origin头部。
使用跨源XMLHttpRequest可以构建基于非同源服务的web应用程序。
通过跨源XMLHttpRequest可以从客户端整合来自不同源的内容,如果目标服务器允许,可以使用用户证书访问受保护的内容,进而让让用户直接访问个人的数据。反之,如果通过服务器端不对同源进行整合,则所有内容都要穿过一个服务器端的基础层,因此可能会形成瓶颈。
其实新版的XMLHttpRequest除了请求不同域名下的数据(跨域请求),还做出以下进步:
1)可以设置HTTP请求的时限。
2)可以使用FormData对象管理表单数据。
3)可以上传文件。
4)可以获取服务器的二进制数据。
5)可以获取数据传输的进度信息。
浏览器支持情况检测
var xhr = new XMLHttpRequest(); if(typeof xhr.withCredentials === undefined){ document.getElementById("support").innerHTML = "you brower does not support" }else{ document.getElementById("support").innerHTML = "you brower support" }
构建跨域请求
XMLHttpRequest,首先要创建一个新的XMLHttpRequest对象,
接下来通过指定不同源的地址来构造跨源的XMLHttpRequest:
在请求过程中务必确保能够监听到错误,请求不成功很多原因,如网络故障、访问被拒、目标服务器缺乏对CORS支持等。
从其他源获取数据的一种常见方式是使用JSONP,使用JSONP设计创建script标签,其中包含指向JSON资源的URL。URL包含了脚本加载时将要调用的函数的名称,有远程服务器负责包装JSON数据,并调用命名函数。这种方式存在重大的安全隐患。因为在使用JSONP时,你必须完全信任服务端所提供的数据,恶意的脚本能够接管你的应用。
构建跨源请求
var xhr = new XMLHttpRequest(); xhr.open("POST", targetLocation, true);
使用进度时间
function() { xhr.upload.onprogress = function(e) { var ratio = e.loaded / e.total; setProgress(ratio + "% 上传中"); }
设置回调函数以处理进度时间,并计算上传和下载的完成率 xhr.onprogress = function(e) { var ratio = e.loaded / e.total; setProgress(ratio + "% 加载中"); } xhr.onload = function(e) { setProgress("上传完毕"); } xhr.onerror = function(e) { setProgress("错误"); } xhr.open("POST", targetLocation, true); geoDataString = dataElement.textContent; xhr.send(geoDataString); }, true);
演示地址:http://lovermap.sinaapp.com/test/crossOriginUpload.html