什么是跨域
我们先看下以下场景:
开启两个本地服务器,页面A为localhost:9800,其中嵌套了iframeB localhost:9000,页面A想使用页面B的数据,例如调用它的方法,会报以下错误
如图所示,Protocols,domains,and ports must match. 译为:协议、主机和端口号必须符合,否则,就是跨域。
下面我们来具体谈谈。
浏览器的同源策略
我们都知道,浏览器有个同源策略,也就是这个策略,限制了两个源中的资源相互交互。
Two pages have the same origin if the protocol, port (if one is specified), and host are the same for both pages.译为:如果两个页面有相同的协议、端口号和主机,那么这两个页面就属于同一个源。
也就是说,只有同一个源才可以进行资源共享。
我们来举几个例子,如果想和 http://www.test.com/index.html 进行通信:
URL | Result | Reason |
---|---|---|
http://www.test.com/page/othe... | 允许 | |
http://www.test.com/index.js | 允许 | |
http://a.test.com/index.html | 不允许 | 子域不同 |
http://www.other.com/index.html | 不允许 | 主域不同 |
https://www.test.com/index.html | 不允许 | 协议不同 |
http://www.test.com:3000/index.html | 不允许 | 端口不同 |
我们可以看到,协议、端口、主机缺一不可,必须完全匹配,上文就是由于端口号不同而报错。
那为什么要有同源策略的限制呢?原因也很简单,就是为了保证用户信息的安全,防止恶意的网站窃取数据。
试想一下,如果我们可以随意访问一个网站的cookie,那么岂不是随意窃取别别人登陆的cookie?如果没有同源策略,那互联网就太危险了。
跨域的几种常见方案
同源策略的限制范围有以下几种:
- Cookie、LocalStorage 和 IndexDB 无法读取。
- DOM 无法获得。
- AJAX 请求不能发送。
而跨域访问,也无非两种情况:一是请求不同源下的接口(如上第3种);二是请求不同源的页面的资源(如上1、2,通常是页面中嵌套不同源的iframe要进行通信)。本文主要讨论第二种情况下造成的跨域,方法很多,主要介绍以下几种:document.domain
、location.hash
、window.name
、postMessage
。
document.domain
适用于:主域相同子域不同的页面,例如上例中的第三种
方法:只需将这两个页面的document.domain
设置为相同的父域名,即可实现不同子域名之间的跨域通信。
<!-- http://www.test.com/index.html -->
<button>发送消息</button>
<div></div>
<iframe name="ifr" src="http://a.test.com/index.html" style="width: 100%;"></iframe>
<script type="text/javascript">
document.domain = 'test.com';
function sendMsg(msg) {
document.querySelector('div').innerHTML = msg;
}
document.querySelector('button').onclick = function (){
window.frames['ifr'].sendMsg('Hello son');
}
</script>
<!-- http://a.test.com/index.html -->
<div></div>
<script type="text/javascript">
document.domain = 'test.com';
function sendMsg(msg) {
document.querySelector('div').innerHTML = msg;
parent.sendMsg('Hello father');
}
</script>
location.hash
一般来说,URL的任何改变都重新会加载一个新的网页,除了hash
的变化,hash
的任何改变都不会导致页面刷新。
在跨域解决方案中,hash
也非常有用,来自不同源的页面可以相互设置对方的 URL,包括hash
值,但不能获取对方的hash
值。文档之间可以通过hash
来相互通信。
流程(如上图):
-
主页面A
中嵌入iframeB
,两个来自不同域 - 在
主页面A
中,将想要传递给B的字段,作为hash,将它与B的url连接起来,然后将B的src设置为连接后的url - 在
iframeB
中,就可以通过获取自己url的hash值,从而得到主页面传递的值,但在iframeB中,需设置一个定时器监听hash值的变化
除了设置定时器,还可以通过监听window.onhashchange
事件
例子:启动两个本地服务器,主页面localhost:9800
,子页面localhost:9000
,主页面向子页面发消息
<!-- 主页面 localhost:9800 -->
<script type="text/javascript">
function sendMsg(msg) {
var data = encodeURI(JSON.stringify({msg: msg}));
var target = "http://localhost:9000";
var src = target + "#" + data;
document.getElementById("ifr").src = src;
}
function onClick() {
sendMsg("来自'localhost: 9800': 你好, 这是你要的数据");
}
</script>
<body>
<iframe id="ifr" src="http://localhost:9000" style="width: 100%"></iframe>
<button onclick="onClick()">发送消息</button>
</body>
<!-- 子页面 localhost:9000 -->
<script type="text/javascript">
var oldHash = "";
function checkHash() {
var newHash = location.hash.length > 1 ? location.hash.substring(1) : '';
if (newHash != oldHash) {
oldHash = newHash;
var msg = JSON.parse(decodeURI(newHash)).msg;
document.getElementById("container").innerText = msg;
clearInterval(timer);
}
}
//设置定时器监听 hash 的变化
var timer = setInterval(checkHash, 1000);
</script>
<body>
<div id="container"></div>
</body>
结果如下:点击button后,子页面将收到主页面的消息
注意:使用hash
时最好对其进行编码、解码,否则在Firefox
中会报错。因为Firefox
会自动将hash
值进行编码,如果不进行解码就无法JSON.parse()
.
两个页面不同源的情况下,IE、Chrome不允许修改parent.location.hash
的值(Firefox可以),所以如果主页面想从子页面获取消息,只能借助一个代理iframe设置。
流程如下(两种):
-
子页面
创建隐藏的代理iframe,与主页面同源,并将消息作为hash
,设置到iframe的src中 -
代理页面
将主页面的hash
值设置为自身的hash
-
主页面
使用定时器监听hash
的变化
例子如下
<!-- 主页面 localhost:9800 -->
<script type="text/javascript">
var oldHash = '';
function checkHash() {
var newHash = location.hash.length > 1 ? location.hash.substring(1) : '';
if (newHash != oldHash) {
oldHash = newHash;
var msg = JSON.parse(decodeURI(newHash)).msg;
document.querySelector("div").innerText = msg;
clearInterval(timer);
}
}
//设置定时器监听 hash 的变化
var timer = setInterval(checkHash, 1000);
</script>
<body>
<iframe id="ifr" name="ifr" src="http://localhost:9000" style="width: 100%;"></iframe>
<div></div>
</body>
<!-- 子页面 localhost:9000 -->
<script type="text/javascript">
function sendMsg(msg) {
try {
parent.location.hash = encodeURI(JSON.stringify({msg: msg}));
} catch(e) {
//由于IE, Chrome的安全机制 无法修改 parent.location.hash
//因此创建代理页面, 与主页面同源, 然后修改代理页面的hash
var ifrProxy = document.createElement("frame");
ifrProxy.style.display = "none";
ifrProxy.src = "http://localhost:9800/public/proxy.html" + '#' + encodeURI(JSON.stringify({msg: msg}));
document.body.appendChild(ifrProxy);
}
}
function onClick() {
sendMsg("Hello Father");
}
</script>
<body>
<button onclick="onClick()">发送消息</button>
</body>
<!-- 代理页面 localhost:9800/public/proxy.html -->
<script type="text/javascript">
parent.parent.location.hash = window.location.hash.substring(1);
</script>
结果如图:
这种方法的劣处就是将消息暴露在url中,因此也可以采用下文将讲述利用代理iframe跨域的方法,这边就不赘述了。
由于现在许多网站的hash
已经被用于其他用途,此时想用hash
跨域就会比较复杂了。这种情况下,我们可以使用一个同域的代理页面来完成
页面A想向页面B发送数据,流程如下:
-
页面A
创建一个隐藏的代理iframeC
,这个iframe与页面B
同域 -
页面A
中,将要发送的数据,作为hash,与页面C
的url
连接起来 - 在
代理iframeC
中,它与B同域,可以直接调用页面B
中的方法,这样便可以将hash值传递给B了
例子如下
<!-- 页面A localhost:9800 -->
<body>
<iframe name="ifr" src="http://localhost:9000" style="width: 100%;"></iframe>
<button onclick="onClick()">发送消息</button>
<div></div>
<script type="text/javascript">
function sendMsg(msg) {
//创建隐藏的iframe, 与页面B同源
var frame = document.createElement('frame');
var target = 'http://localhost:9000/static/page/proxy.html';
var data = {frameName: 'ifr', msg: msg};
frame.src = target + "#" + encodeURI(JSON.stringify(data));
frame.style.display = 'none';
document.body.appendChild(frame);
}
function onClick() {
sendMsg('你好, 我是localhost: 9800');
}
</script>
<!-- 代理页面C localhost:9000/static/page/proxy.html -->
<script type="text/javascript">
var hash = location.hash.length > 1 ? location.hash.substring(1) : '';
if (hash) {
var data = JSON.parse(decodeURI(hash));
//处理数据
parent.frames[data.frameName].receiveData(data.msg);
}
</script>
<!-- 页面B localhost:9000 -->
<script type="text/javascript">
function receiveData(msg) {
document.querySelector('div').innerHTML = "接收到数据:"+ msg;
}
</script>
<body>
<div></div>
</body>
结果如下:
缺点:
- 数据直接暴露在url中,安全性较低
- url大小是有限制的,它支持传递的数据量较小
window.name
加载任何页面 window.name 的值始终保持不变
当页面A想从页面B中获取数据时:
-
页面A
,创建一个隐藏的iframeC
,将C的src
指向页面B
-
页面C
加载完成后,把响应的数据附加到window.name
上 - C 取到数据后,将
src
设为任何一个与A同源的页面
,这时 A 就能获取到 B 的name
属性值 - A 取到数据后,随时可以删掉 C
例子,启动两个本地服务器,页面Alocalhost:9800
,页面Blocalhost:9000
,页面A想从页面B中获取数据
<!-- 页面A localhost:9800 -->
<script type="text/javascript">
function sendMsg(msg) {
var state = 0, data;
//1. 创建隐藏的iframe, 与页面B同源
var frame = document.createElement("frame");
frame.src = "http://localhost:9000";
frame.style.display = "none";
frame.onload = function () {
if (state === 1) {
//3. 此时iframe与页面A同源, 页面A可以获取到数据
data = frame.contentWindow.name;
document.querySelector('.res').innerHTML = "响应数据:" + data;
//4. 删除iframe
frame.contentWindow.document.write('');
frame.contentWindow.close();
document.body.removeChild(frame);
} else {
//2. iframe加载完成后, 响应的数据已附加到此iframe上, 再将其导航至与页面A同源
state = 1;
frame.src = "http://localhost:9800/test.html";
}
};
document.body.appendChild(frame);
}
function onClick() {
var val = 'hi, 我是页面A';
sendMsg(val);
document.querySelector('.val').innerHTML = "请求数据:" + val;
}
</script>
<body>
<button onclick="onClick()">发送消息</button>
<div class="val"></div>
<div class="res"></div>
</body>
<!-- 页面B localhost:9000 -->
<script type="text/javascript">
window.name = 'hi, 我是页面B';
</script>
结果如图
另外,两个页面还可相互通信
页面A通过hash
,将数据传递给页面B,页面B仍通过window.name
向页面A传递数据
场景如下:页面B存储了一些人的信息,页面B通过页面A的输入,获取不同人的信息
<!-- 页面A localhost:9800 -->
<script type="text/javascript">
function sendMsg(msg) {
var state = 0, data;
//1. 创建隐藏的iframe, 与页面B同源
var frame = document.createElement("frame");
frame.style.display = "none";
//将获取的数据通过hash传递给页面B
frame.src = "http://localhost:9000" + "#" + encodeURI(JSON.stringify({data: msg}));
frame.onload = function () {
if (state === 1) {
//3. 此时iframe与页面A同源, 页面A可以获取到数据
data = JSON.parse(frame.contentWindow.name);
var html = '';
for (var key in data) {
html += key + ": " + data[key] + " ";
}
document.querySelector('.res').innerHTML = html;
//4. 删除iframe
frame.contentWindow.document.write('');
frame.contentWindow.close();
document.body.removeChild(frame);
} else {
//2. iframe加载完成后, 响应的数据已附加到此iframe上, 再将其导航至与页面A同源,
state = 1;
// !!!注意 '/test.html'不可缺少,否则在firefox不会触发frame.onload, 在chrome虽会触发但会报错
frame.src = "http://localhost:9800/test.html";
}
};
document.body.appendChild(frame);
}
function onClick() {
var val = document.querySelector('.text').value;
sendMsg(val);
document.querySelector('.val').innerHTML = "id为 " + val + " 的信息如下:";
}
</script>
<body>
<input class="text" type="text" />
<button onclick="onClick()">发送消息</button>
<div class="val"></div>
<div class="res"></div>
</body>
<!-- 页面B localhost:9000 -->
<script type="text/javascript">
var information = {
'1': {
name: '狐狸',
age: 2
},
'2': {
name: '馒头',
age: 1
},
'3': {
name: '端午',
age: 3
}
};
var hash = location.hash.length > 1 ? location.hash.substring(1) : '';
if (hash) {
//获取hash值 并进行转换
var obj = JSON.parse(decodeURI(hash));
var data = obj.data;
//传递的数据为object, 需进行JSON转换
window.name = JSON.stringify(information[data]);
}
</script>
输入想获得的数据的id值,即可得到相应的信息
总结:
优点:容量很大,可以放置很长的字符串(2M左右,比url大得多)
缺点:必须要监听window.name属性的变化
window.postMessage()
HTML5规范中的新方法window.postMessage()
可以用于安全跨域通信。当该方法调用时,将分发一个消息事件。对应的窗口通过事件监听来获取消息。
语法 :otherWindow.postMessage(message, targetOrigin)
otherWindow
代表其他窗口的引用,例如iframe的contentWindow
属性,通过window.open
返回的窗体,通过window.frames[]
返回的ifame对象。
message
表示发送给其他窗口的数据
targetOrigin
指定哪些来源的窗口可以接收到消息事件,其值可以是字符串"*"(表示无限制)或"/"(表示与父窗口同源)或一个URI。发送消息时,只有目标窗口的协议
、主机
、端口
这三项都匹配targetOrigin提供的值,消息才会发送。
接收消息的窗口可以通过监听message事件
,去接收消息
window.addEventListener("message", receiveMessage, false);
receiveMessage
为接收到消息后的操作
message事件的事件对象event
,提供三个属性:
-
event.source
:发送消息的窗体(可以用来引用父窗口) -
event.origin
:消息发向的网址(可以过滤不是发给本窗口的消息) -
event.data
:消息内容
例子:启动两个本地服务器,页面Alocalhost:9800
,页面Blocalhost:9000
,页面A根据页面B发来的数据改变颜色
<!-- 页面A localhost:9800 -->
<body>
<iframe name="ifr" src="http://localhost:9000" style="width: 100%; display: none"></iframe>
<div style="width: 200px; height: 200px; background-color: #ccc"></div>
<button onclick="onClick()">改变颜色</button>
<script type="text/javascript">
function onClick() {
var otherFrame = window.frames["ifr"];
otherFrame.postMessage("getColor", "*");
}
function handleReceive(event){
//判断来源
if(event.origin != "http://localhost:9000")
return;
//处理页面B发送的数据
var data = JSON.parse(event.data);
document.querySelector('div').style.backgroundColor = data.color;
}
window.addEventListener("message", handleReceive, false);
</script>
<!-- 页面B localhost:9000 -->
<script type="text/javascript">
function handleReceive(event){
//判断来源
if (event.origin != "http://localhost:9800") {
return;
}
if(event.data == "getColor"){
//给页面A发送数据
var targetWindow = parent.window;
targetWindow.postMessage(JSON.stringify({color:'blue'}), "http://localhost:9800");
}
}
window.addEventListener("message", handleReceive, false);
</script>
结果如下:点击按钮后,页面的div由灰变蓝
结语
关于跨域的就先聊到这啦~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。