YeaseonZhang

前端跨域常用方法

在开发过程中经常会涉及跨域问题,解决跨域问题的方案也有很多种,接下来就来梳理一下前端跨域的常用方法。

同源策略

何为跨域跨域是相对于同源而言。协议、域名和端口均相同,则为同源
浏览器通过同源策略限制从一个源加载的文档或脚本与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制,摘抄自MDN

常见解决方案

document.domain

这种方案主要用于主域相同,子域不同的跨域情况。例如: https://jdc.jd.com/https://www.jd.com/

通过在https://www.jd.com/打开一个https://jdc.jd.com/,此时JDC的域名是jdc.jd.com/,通过控制台执行document.domain = 'jd.com';。强制设置主域,实现同源。

1
2
3
4
5
var jdc = window.open('https://jdc.jd.com/');
// JDC 页面加载完成后执行
var divs = jdc.document.getElementsByTagName('div');

$(divs).css('border', '1px solid red');

通常的做法是通过iframe加载一个跨域页面资源。因为window.open这个方法在浏览器中会被当做谈广告禁止掉。

http://domain.com/index.html

1
2
3
4
<iframe id="sub" src="http://sub.domain.com/index.html"></iframe>
<script>
var username = 'yeseonzhang';
</script>

http://sub.domain.com/index.html

1
2
3
4
<script>
document.domain = 'domain.com';
console.log(window.parent.username);
</script>

location.hash

这种跨域方法主要是通过设置/监听url的hash部分,来实现跨域,同时需要借助第三个页面来进行辅助。

上图就是三个页面的包含关系,以及hash的传递过程。

http://domain-a.com/a.html

1
2
3
4
5
6
7
8
9
10
11
<iframe id="iframe-b" src="http://domain-b.com/b.html"></iframe>
<script>
var bPage = document.getElementById('iframe-b');

/* step 1 */
bPage.src = bPage.src + '#user=yeaseonzhang';

function cb (res) {
console.log(res);
}
</script>

http://domain-b.com/b.html

1
2
3
4
5
6
7
8
9
<iframe id="iframe-c" src="http://domain-a.com/c.html"></iframe>
<script>
var cPage = document.getElementById('iframe-c');

window.onhashchange = function () {
/* step 2 */
cPage.src = cPage.src + location.hash;
}
</script>

http://domain-a.com/c.html

1
2
3
4
5
6
<script>
window.onhashchange = function () {
/* step 3 */
window.parent.parent.cb('success: ' + location.hash);
}
</script>

由于a页面c页面是同域资源,所以c页面可以通过window.parent.parent访问a页面资源。

window.name

这个方案类似location.hash,需要通过第三个页面进行辅助。
window.name属性是用来获取/设置窗口的名称。需要注意的是,当前窗口的window.name并不会因为页面的重载和跳转而更改,所以可以利用这个特性将跨域的window.name通过重定向到同域页面进行读取。

http://domain-a.com/a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
var iframe = document.createElement('iframe');
/* step 1 加载跨域页面 */
iframe.src = 'http://domain-b.com/b.html';
var domain = 'diff';

/* 监听iframe加载 */
iframe.onload = function () {
if ('diff' == domain) {
/* step 2 重定向到同域页面 */
iframe.contentWindow.location = 'http://www.domain-a.com/c.html';
domain = 'same';
} else if ('same' == domain) {
/* 获取同域资源的window.name信息 */
cb(iframe.contentWindow.name);
/* 清空数据 */
iframe.contentWindow.name = '';
}
}

function cb (res) {
console.log(JSON.parse(res));
}
</script>

http://domain-b.com/b.html

1
2
3
4
5
6
7
<scirpt>
/* 写入相关数据 */
var obj = {
username: 'yeaseonzhang'
}
window.name = JSON.stringify(obj);
</script>

http://domain-a.com/c.html

同域c页面,可以是一个空页面,不需要进行任何操作。

JSONP

JSONP(JSON with Padding)是JSON的一种使用方式。这种方式允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据。

众所周知,html页面中所有带有src属性的标签(<img>,<script>iframe)都拥有跨域能力。所以最简单的实现方式就是动态加载JS。

客户端

1
2
3
4
5
6
7
8
function todo(data){
console.log('The author is: '+ data.name);
}

var script = document.createElement('script');
/* callback参数,用来指定回调函数的名字。 */
script.src = 'http://www.yeaseonzhang.com/author?callback=todo';
document.body.appendChild(script);

服务端

1
2
/* 服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。 */
todo({"name": "yeaseonzhang"});

todo()函数会被作为全局函数来执行,只要定义了todo()函数,该函数就会被立即调用。

postMessage

window.postMessage是HTML5中一个安全的,基于事件的消息API。

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

postMessage(),方法包含三个参数:

  • message: 消息内容
  • targetOrigin: 接受消息窗口的源,即”协议 + 域名 + 端口”。也可以设置为通配符*,向所有窗口发送
  • transfer: 可选参数(布尔值),是一串和message 同时传递的Transferable对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

发送者接收者都可以通过message事件,监听对方的消息。message事件的事件对象event包含三个属性:

  • event.source: 发送消息的窗口对象的引用,可以用此在两个窗口建立双向通信。
  • event.origin: 发送消息的URI
  • event.data: 消息内容

发送者: http://domain-a.com/a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
var newWindow = window.open('http://domain-b.com/b.html');
/* 向b.html发送消息 */
newWindow.postMessage('Hello', 'http://domain-b.com/b.html');

/* 双向通信,接收b.html的回复消息 */
var onmessage = function (event) {
var data = event.data;
var origin = event.origin;
var source = event.source;
if (origin == 'http://domain-b.com/b.html') {
console.log(data); //Nice to see you!
}
};
window.addEventListener('message', onmessage, false);
</scirpt>

接收者:http://domain-b.com/b.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
var onmessage = function (event) {
var data = event.data;
var origin = event.origin;
var source = event.source;
if (origin == 'http://domain-a.com/a.html') {
console.log(data); //Hello
/* 回复a.html的消息 */
source.postMessage('Nice to see you!', 'http://domain-a.com/a.html');
}
};
window.addEventListener('message', onmessage, false);
</script>

WebSocket

WebSocket是一种HTML5的一种新的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案,详细介绍请访问MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* websocket协议为ws/wss, 类似http/https的区别 */
wsUrl = 'wss://127.0.0.1:8090/ws/';

/* 发送 */
ws = new WebSocket(wsUrl);

/* 连接成功建立时调用 */
ws.onopen = function (event) {
console.log("websocket command onopen");
var msg = {
username: 'YeaseonZhang'
}
/* 通过 send() 方法向服务端发送消息,参数必须为字符串 */
ws.send(JSON.stringify(msg));
};

/* 服务端向客户端发送消息时调用 */
ws.onmessage = function (event) {
/* event.data包含了服务端发送过来的消息 */
console.log("websocket command onmessage: " + event.data);
if (event.data === 'success') {
/* 通过 close() 方法断开websocket连接 */
ws.close();
}
};

/* 连接被关闭时调用 */
ws.onclose = function (event) {
console.log("websocket command onclose: " + event.data);
};

/* 出现错误时调用 */
ws.onerror = function (event) {
console.log("websocket command onerror: " + event.data);
};

WebSocket的优势是除了可以实现跨域,还有就是可以保持长连接,而不需要通过轮询实现实时性。

CORS

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

只需要后端同学支持就ok,前端不需要做很多额外工作(除了携带cookie)。

只要服务器返回的相应中包含头部信息Access-Control-Allow-Origin: domain-namedomain-name为允许跨域的域名,也可以设置成*,浏览器就会允许本次跨域请求。

结语

以上就是我所了解的跨域的解决方案,希望对你有所帮助。