iframe学习(四)之窗口跨域 什么是跨域? iframe跨域解决方案 参考

iframe学习(四)之窗口跨域
什么是跨域?
iframe跨域解决方案
参考

跨域问题是浏览器同源策略限制,当前域名的JavaScript只能读取同域下的页面对象,这也是JavaScript出于安全方面的考虑。

通俗的来说,跨域可以理解为:从一个域名访问另一个域名,出于安全考虑,浏览器不允许这么做。

跨域种类

什么时候我们认为发生了跨域呢?或者说什么情况下“浏览器”是不允许我们访问的呢?

不同域名 - 禁止

例子:http://www.baidu.com 与 http://www.h5course.com

不同子域 - 禁止

例子:http://www.baidu.com 与 http://play.baidu.com

不同端口 - 禁止

例子:http://www.h5course.com 与 http://www.h5course.com:8080

不同协议 - 禁止

例子:http://www.baidu.com 与 https://www.baidu.com

跨域表现

  • 无法读取cookie、localStorage、indexDB

  • DOM无法获得

  • ajax请求无法发送

iframe跨域解决方案

如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。

比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。

document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父窗口想获取子窗口的DOM,因为跨源导致报错。

反之亦然,子窗口获取主窗口的DOM也会报错。

window.parent.document.body
// 报错

情况一:两个窗口一级域名相同,只有二级域名不同

方案一:document.domain

document.domain用来设置获取当前网页的域名。

比如在百度(https://www.baidu.com)页面控制台中输入:

alert(document.domain); //"www.baidu.com"

我们也可以给document.domain属性赋值,不过是有限制的,你只能赋成当前的域名或者一级域名,比如:

alert(document.domain = "baidu.com"); //"baidu.com"
alert(document.domain = "www.baidu.com"); //"www.baidu.com"

上面的赋值都是成功的,因为www.baidu.com是当前的域名,而baidu.com是一级域名。

但是下面的赋值就会出来"参数无效"的错误,比如:

alert(document.domain = "qq.com"); //参数无效 报错
alert(document.domain = "www.qq.com"); //参数无效 报错

因为qq.com与baidu.com的一级域名不相同,所以会有错误出现,这是为了防止有人恶意修改document.domain来实现跨域偷取数据。

利用document.domain 实现跨域

前提条件:这两个域名必须属于同一个一级域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域。

Javascript出于对安全性的考虑,而禁止两个或者多个不同域的页面进行互相操作,而相同域的页面在相互操作的时候不会有任何问题。

同源策略会判断两个文档的原始域是否相同来判断是否跨域。这意味着只要把这个值设置成一样就可以解决跨域问题了!!!

【情况一:一级域名不相同,无法使用document.domain】
baidu.com的一个网页(baidu.html)里面利用iframe引入了一个qq.com里的一个网页(qq.html)。

这时在baidu.html里面可以看到qq.html里的内容,但是却不能利用javascript来操作它。因为这两个页面属于不同的域,在操作之前,js会检测两个页面的域是否相等,如果相等,就允许其操作,如果不相等,就会拒绝操作。

这里不可能把baidu.html与qq.html利用JS改成同一个域的。因为它们的一级域名不相等。(强制用JS将它们改成相等的域的话会报跟上面一样的"参数无效错误。")

但如果在baidu.html里引入baidu.com里的另一个网页,是不会有这个问题的,因为域相等。

【情况二:一级域名相同,可以使用document.domain】
news.baidu.com(news.html)

map.baidu.com(map.html

news.baidu.com里的一个网页(news.html)引入了map.baidu.com里的一个网页(map.html)

这时news.html里同样是不能操作map.html里面的内容的,因为document.domain不一样,一个是news.baidu.com,另一个是map.baidu.com。这时我们就可以通过Javascript,将两个页面的domain改成一样的,需要在a.html里与b.html里都加入:

document.domain = “baidu.com”;

这样这两个页面就可以互相操作了。也就是实现了同一一级域名之间的"跨域"。

例子:

// news.baidu.com下的news.html页面:
<script>
    document.domain = 'baidu.com';
    var ifr = document.createElement('iframe');
    ifr.src = 'map.baidu.com/map.html';
    ifr.style.display = 'none';
    document.body.appendChild(ifr);
    ifr.onload = function(){
        var doc = ifr.contentDocument || ifr.contentWindow.document;
        // 这里可以操作map.baidu.com下的map.html页面
        var oUl = doc.getElementById('ul1');
        alert(oUl.innerHTML);
        ifr.onload = null;
    };
</script>


// map.baidu.com下的map.html页面:
<ul id="ul1">我是map.baidu.com中的ul</ul>
<script>
    document.domain = 'baidu.com';
</script>

情况二:两个窗口完全不同源

目前有三种方法,可以解决跨域窗口的通信问题。

  • 片段识别符(fragment identifier)

  • window.name

  • 跨文档通信API(Cross-document messaging)

方案一:片段识别符

片段标识符(fragment identifier)指的是,URL的#号后面的部分包括#,可以使用window.location.hash获取,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。

父窗口可以把信息,写入子窗口的片段标识符。

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

子窗口通过监听hashchange事件得到通知。

window.onhashchange = checkMessage;

function checkMessage() {
var message = window.location.hash;
    // ...
}

同样的,子窗口也可以改变父窗口的片段标识符。

parent.location.href= target + "#" + hash;

小课堂

hashchange事件

当URL的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的URL部分,包括#符号)

用JavaScript模拟该事件的脚本,原理基本上都是隔一段时间检测一下location.hash是否发生变化

(function(window) {

  // 如果浏览器原生支持该事件,则退出  
if ( "onhashchange" in window.document.body ) { return; }

  var location = window.location,
    oldURL = location.href,
    oldHash = location.hash;

  // 每隔100ms检测一下location.hash是否发生变化
  setInterval(function() {
    var newURL = location.href,
      newHash = location.hash;

    // 如果hash发生了变化,且绑定了处理函数...
    if ( newHash != oldHash && typeof window.onhashchange === "function" ) {
      // execute the handler
      window.onhashchange({
        type: "hashchange",
        oldURL: oldURL,
        newURL: newURL
      });

      oldURL = newURL;
      oldHash = newHash;
    }
  }, 100);

})(window);

方案二:window.name

window.name是一个所有浏览器都有的属性,表示浏览器窗口的名称,默认是一个空字符串,所有浏览器都是个空字符串,这是一个可读可写的属性,语法如下:

string = window.name;

window.name = string;

(一)为什么可以进行跨域操作?

每一个页面都对应一个window窗口,一个窗口可以打开不同的页面,无论这些页面同域还是不同域,它们都共享一个window.name,这一点是实现跨域的关键。

window.name有个很有意思的跨页面特性,具体描述为:页面如果设置了window.name,即使进行了页面跳转到了其他页面,这个window.name还是会保留,也就是说能够记忆来源页面设置的window.name值,这个可比document.referrer还要好用,毕竟可以直接指定任意字符,而document.referrer还需要对URL进行处理。

(二)跨域特性校验

1、首先在一个窗口打开蚂蚁部落(www.softwhy.com)。

2、然后在谷歌开发者工具控制台设置window.name的值为"蚂蚁部落":

iframe学习(四)之窗口跨域
什么是跨域?
iframe跨域解决方案
参考

3、然后在同一个窗口打开百度的首页,很明显这不同域的

在此窗口下的控制台获取设置的window.name属性值:

iframe学习(四)之窗口跨域
什么是跨域?
iframe跨域解决方案
参考

可以看到无论是否同域,都会共享一个window.name

如果在之后所有载入的页面都没对window.name进行修改的话,那么所有的这些页面获取到的window.name的值都是a.html页面中设置的那个值

(三)跨域操作 

通过下面的代码实例简单演示一下利用window.name实现跨域操作。

假设有如下三个页面:

(1).x.com/getDate.html:获取数据的页面

(2).x.com/proxy.html:代理页面,与获取数据页面同域

(3).y.com/date.html:数据页面,getDate.html将从其获取数据

我们的需求是getDate.html利用window.name从date.html页面获取相关数据。

date.html页面中的数据如下:

let date = {
    webName:"蚂蚁部落",
    address:"青岛市南区"
}
window.name = date;

window.name数据可以共享,当前窗口加载date.html页面后,再加载getDate.html页面。

getDate.html页面可以获取date.html中设置的数据,但是总不能采取如下两种措施:

(1)date.html有代码可以在同一窗口跳转到getDate.html页面。

(2)打开date.html页面,然后再手动在同一窗口打开getDate.html页面。

上述两种方式过于死板,在实际项目中使用不太现实,不过可以利用<iframe>另辟蹊径。

实现跨域操作步骤如下:

(1)在getDate.html页面创建一个隐藏的<iframe>:

动态创建一个<iframe>,并将其设置为隐藏状态,不影响页面的正常布局。

并将此<iframe>的src属性值设置为y.com/date.html,那么框架的window.name会获取到对应的数据。

(2)监听<iframe>的load事件,进行相应操作:

由于是跨域getDate.html页面无法获取在<iframe>加载的y.com/date.html中的数据。

但是可以当y.com/date.html页面在框架中加载完成之后,再在框架加载proxy.html页面。

由于proxy.html和getDate.html共享window.name,并且proxy.html与getDate.html是同域的。

(3)核心代码展示:

下面给出操作的核心代码部分,比较简单:

let state = 0, 
    iframe = document.createElement('iframe'),
    loadfn = ()=> {
        if (state === 1) {
            // 读取数据
            let data = iframe.contentWindow.name;    
            // 其他代码
        } else if (state === 0) {
            state = 1;
            // 加载同域代理文件
            iframe.contentWindow.location = "http://x.com/proxy.html"; 
        }  
    };
 
iframe.src = 'http://y.com/date.html';
if (iframe.attachEvent) {
  iframe.attachEvent('onload', loadfn);
} else {
  iframe.onload  = loadfn;
}
document.body.appendChild(iframe);

上述代码比较简单,下面做简略说明:

(1)动态创建一个iframe,并初始化一个状态标记state。

(2)初始设置iframe加载跨域date.html页面。

(3)通过load事件监听iframe加载的页面是否已经加载完成,如果完成则执行loadfn函数。

(4)首先加载的是跨域数据文件,加载完成后执行loadfn函数,如果state等于0,那说明没有加载同域代理文件,于是通过iframe.contentWindow.location = "http://x.com/proxy.html"在iframe窗口加载同域代理文件,并将 state值设置为1。

(5)如果同域代理文件在iframe中加载完成,再次出发load时间,再执行loadfn函数,此时state变为1,表示同域代理文件加载完成,你可以顺畅的获取数据了。

删除iframe:

当我们获取数据并进行相关操作完毕后,那么动态创建的iframe也就无用了。

可以通过如下JavaScript将其删除:

iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);

可根据需要进行修改

小课堂

window.name属性

【容纳数据量】

window.name属性可以容纳的数据大小大致是2M,数据量还是可以的,不同的浏览器可能会有所不同

【新窗口打开的window.name无效】

window就是窗口的意思,所以上面的<a>链接如果我们设置了target="_blank"新窗口打开,则目标页面的window.name就是空字符串'',因为是新的窗口,不是那个设置了window.name的窗口。

因此,window.name跨页面传递数据还是有一定限制的。

【其它特性】

  • window.name可读可写,只支持字符串;

  • window.name的值跟着浏览器窗口走的,不是跟着页面走的;

  • window.name没什么卵用,知道他没用就是很有用的知识。

【window.name与跨域与没什么卵用了】

window.name的值是跟着浏览器窗体走的,因此,只要在一个窗体中,就可以共享一个值,于是可以实现跨域数据获取,这个在以前还是挺有名的一个跨域方法,名叫“window.name Transport”,有兴趣可以参考这个2008年的老文,这里不展开,这个跨域方法要比JSONP要安全些。

然后,我要讲下问什么不展开了,因为现在使用window.name实现跨域通信已经属于鸡肋方法了,请使用postMessage跨域跨文档通信代替,更好用更安全更强大。

有此看来,现在window.name这个属性已经没什么卵用了,除了上面提到了偶尔可以用来在同一窗口前后页面之间做简单的数据传递,包括JSON字符串数据。

window.name = '{ "foo": "bar" }';

方案三:window.postMessage

HTML5为了解决跨域问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。

这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

(一)父窗口向子窗口发送消息

举例来说,父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage方法就可以了。

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

(二)子窗口向父窗口发送消息

window.opener.postMessage('Nice to see you', 'http://aaa.com');

(三)父窗口和子窗口都可以通过message事件,监听对方的消息

window.addEventListener('message', function(e) {
    console.log(e.data);
},false);

message事件的事件对象event,提供以下三个属性。

  • event.source:发送消息的窗口

  • event.origin: 消息发向的网址

  • event.data: 消息内容

下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。

window.addEventListener('message', receiveMessage);

function receiveMessage(event) {
    event.source.postMessage('Nice to see you!', '*');
}

event.origin属性可以过滤不是发给本窗口的消息。

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
    if (event.origin !== 'http://aaa.com') return;
    if (event.data === 'Hello World') {
        event.source.postMessage('Hello', event.origin);
    } else {
        console.log(event.data);
    }
}        

(四)读写其它窗口的LocalStorage

父窗口发送消息的代码如下:

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');

上面代码中,子窗口将父窗口发来的消息,写入自己的LocalStorage。

子窗口接收消息并设置localstorage代码如下:

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

加强版的父窗口发送消息代码如下:

var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 将主窗口数据存入子窗口的localstorage
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
// 读取子窗口数据
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
// 读取子窗口数据的监听 window.onmessage
= function(e) { if (e.origin != 'http://aaa.com') return; // "Jack" console.log(JSON.parse(e.data).name); };

加强版的子窗口接收消息的代码如下:

window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

小课堂

window.postMessage()方法

【语法】

otherWindow.postMessage(message, targetOrigin, [transfer])

【参数】

  • otherWindow

    其他窗口的一个引用,写的是你要通信的window对象。

    例如在iframe中向父窗口传递数据时,可以写成window.parent.postMessage(),window.parent表示父窗口。

  • message

    需要传递的数据,字符串或者对象都可以。

  • targetOrigin

    表示目标窗口的源,协议+域名+端口号,如果设置为“*”,则表示可以传递给任意窗口。在发送消息的时候,如果目标窗口的协议、域名或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。

  • [transfer]

    可选参数。是一串和message 同时传递的 Transferable 对象,这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。我们一般很少用到。

window.opener()方法

返回打开当前窗口的那个窗口的引用,例如:在window A中打开了window B,B.opener 返回 A.

【语法】

var objRef = window.opener;

【例子】

if (window.opener != indexWin) {
     referToTop(window.opener);
 }

【备注】

如果当前窗口是由另一个窗口打开的, window.opener保留了那个窗口的引用. 如果当前窗口不是由其他窗口打开的, 则该属性返回 null.

Window.open()方法

【语法】

window.open(strUrlstrWindowName, [strWindowFeatures]);

【参数】

strUrl === 要在新打开的窗口中加载的URL。

strWindowName === 新窗口的名称。

strWindowFeatures === 一个可选参数,列出新窗口的特征(大小,位置,滚动条等)作为一个

message事件

用 message 事件通知一个目标对象(WebSocketRTCDataChannelEventSource ,BroadcastChannel )它接收到了一个信息。

参考

MDN hashchange

张鑫旭 快速了解window.name特性与作用

蚂蚁部落 window.name 跨域

iframe与父窗口交互(不同源不能拿到彼此的dom)

MDN Window.open()

MDN window.opener()

MDN message