跨域访问常用方法

跨域访问问题

由于浏览器同源策略的限制,一个域下的 JavaScript 无法访问另一个域,包括提交、获取等操作。这里的同源指的是协议、域名、端口都相同,其中即便是域名和域名对应的 IP 之间浏览器也不允许访问。对于端口和协议不同的跨域访问,只能在后端处理。

可以大致将跨域访问分为两大类,一类是需要后端配合处理跨域的,一类是利用 <iframe> 实现的跨域。

常用方法

JSONP

我们知道 JSON 是一种数据交换格式,而 JSONP(JSON with Padding)是一个非正式的跨域数据交互协议。受浏览器同源策略的影响,使用 AJAX 不能直接访问非同源的资源,但是 JavaScript 的 <script> 不受同源策略限制(JS 你的良心不会痛吗)。于是我们可以使用 <script> 的 src 指向另一个域的地址并附带一个参数指定回调函数名,并提供这个回调函数来处理接收的数据,这个地址的服务器端后台处理这个请求并将数据返回,由于 JSON 的天然优势(纯文本、跨平台、JS 原生支持等),遂将数据用回调函数名包裹封装成 JSON 返回。浏览器请求这个 <script> 后会得到服务器端返回的数据,执行回调函数。这样说起来有点费劲,还是代码清楚。

我这里使用 Node.js 来模拟跨域环境。

首先是后台逻辑,运行该脚本将启动一个 http 服务器,使用 http://127.0.0.1:8888 可以访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const http = require('http');
const url = require("url");

http.createServer(function (request, response) {
//封装数据
const data = {
status: 'success'
}
//取传递过来的回调函数名
const callback = url.parse(request.url, true).query.callback;
//返回数据
response.end(`${callback}(${JSON.stringify(data)})`);

}).listen(8888, '127.0.0.1');

console.log('--------http server start on 8888 port--------')

httpserver

然后是前端的页面。这里要使用到 Node.js 的 http-server。使用 npm install -g http-server(加-g 参数全局安装)安装 http-server,然后在前端页面所在目录运行 http-server
http_server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>firstpage</title>
</head>
<body></body>
<script>
//回调函数
function callback(data) {
console.log(data)
}
</script>
<script src="http://127.0.0.1:8888?callback=callback"></script>
</html>

result

可以看到,http://192.168.1.102:8080/firstpage.html 中的脚本访问 http://127.0.0.1:8888,并成功获取到了数据。

如果选择使用 jQuery 就可以免去定义 <script> 和回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>firstpage</title>
</head>
<body></body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$.ajax({
url: 'http://127.0.0.1:8888',
type: 'get',
dataType: 'jsonp',
jsonp: 'callback',//传递给后端的回调函数的参数名称
jsonpCallback: 'callback',//回调函数的名称
success: function (data) {
console.log(data)
},
error: function (msg) {
console.log(msg)
}
})
</script>
</html>

乍一看 AJAX 与 JSONP 很相似,但是它俩最本质的区别就是 AJAX 是通过 XMLHttpRequest 对象获取非本页数据(同源),而 JSONP 是通过 JavaScript 的 <script> 标签获取非本页数据(同源、不同源都可以)。

JSONP 的缺点

  • 可能会有安全隐患。
  • 只能使用 GET 请求。

CORS

CORS 是 W3C 的标准,全称是”跨域资源共享”(cross-origin resource sharing)。它允许浏览器向跨源服务器发出 XMLHttpRequest 请求,从而克服了 ajax 只能同源使用的限制。

CORS 需要浏览器和服务器同时支持才可以生效,所以可能会存在兼容性的问题。服务器实现了 CORS 接口,选择兼容的浏览器就可以实现跨域通信,并且开发者完全可以用之前的方式使用 XMLHttpRequest 而感觉不到跨域限制的存在。

简单来说,CORS 通过在跨域地址的响应头中设置 Access-Control-Allow-Origin 的值为允许跨域访问的地址,一个域在发起跨域访问时,浏览器请求该地址并检测响应头中的 Access-Control-Allow-Origin 值,如果包含该域,则允许访问。发起请求的 JS 对象在某些 IE 中不是 XMLHttpRequest 而是 XDomainRequest,因此可以使用 jQuery 等工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http');
const url = require("url");

http.createServer(function (request, response) {
//封装数据
const data = {
status: 'success'
}
//取传递过来的回调函数名
const callback = url.parse(request.url, true).query.callback;
response.writeHead(200, {
'Access-Control-Allow-Origin': 'http://127.0.0.1:8080'
});
//返回数据
response.end(`${JSON.stringify(data)}`);

}).listen(8888, '127.0.0.1');

console.log('--------http server start on 8888 port--------')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CORS</title>
</head>
<body></body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$.ajax({
url: 'http://127.0.0.1:8888',
type: 'get',
dataType: 'json',
success: function (data) {
console.log(data)
},
error: function (msg) {
console.log(msg)
}
})
</script>
</html>

CORS

CORS 的优点

  • CORS 可以处理任意类型的请求,而 JSONP 只支持 GET 请求。
  • CORS 能够更好的处理错误,而 JSONP 对于请求出错不能很好的处理。
  • CORS 更为安全。

服务器代理

听起来高大上,其实就是 JS 将跨域请求发送给同源的后端,由后端代为请求,然后将获取的数据封装返回。

location.hash

需要配合 <iframe> 使用,并且使用复杂,问题较多,暂不考虑。

document.domain

使用这种方式只适用两个主域名相同,子域名不同的域间通信,例如:www.test.com 和 sub.test.com 之间通信。

模拟这种环境,除了跨域环境,还要进行域名映射,这里使用 nginx。

a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body></body>
<script>
document.domain = 'test.com';
let iframe = document.createElement('iframe');
iframe.src = 'http://sub.test.com/b.html';
iframe.style.display = 'none';
document.body.append(iframe);
iframe.onload = function () {
let win = iframe.contentWindow;
console.log(win.data);
}
</script>
</html>

b.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>b.html</title>
</head>
<body></body>
<script>
document.domain = 'test.com';
window.data = 'hello, cross domain';
</script>
</html>

首先开启 http-server

document.domain_http_server

www.test.com 和 sub.test.com 都映射到 http://192.168.0.141:8081。

nginx 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name www.test.com;
location / {
proxy_pass http://192.168.0.141:8081;
}

}
server {
listen 80;
server_name sub.test.com;
location / {
proxy_pass http://192.168.0.141:8081;
}
}
}

www.test.com 这个域名在互联网上可能已经被注册,因此需要在 hosts 中指定我们要访问的并不是互联网上的这个地址,而是 http-server 部署的地址。

1
2
3
# set nginx hosts
192.168.0.141 www.test.com
192.168.0.141 sub.test.com

经过上面的配置,在客户端访问 www.test.com 和 sub.test.com 时,都会通过 nginx 的反向代理去请求真实的地址:http://192.168.0.141:8081。

nginx_proxy_broswer

window.name

window 对象有个 name 属性,该属性在一个窗口(window)的生命周期内载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body></body>
<script>
const iframe = document.createElement('iframe');
iframe.src = 'http://192.168.124.177:8081/b.html';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function () {
iframe.onload = function () {
var data = iframe.contentWindow.name;
console.log(data);
}
// 这里需要指定一个同源的路径
// 目的是让 http://192.168.0.141:8081/a.html 能够访问到 iframe 中的数据
iframe.src = 'http://192.168.0.141:8081';
}
</script>
</html>
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>b.html</title>
</head>
<body></body>
<script>
window.name="this is data";
</script>
</html>

http://192.168.0.141:8081/a.html 访问 http://192.168.124.177:8081/b.html 中的提供的数据。

window_name

window.name 的优点

  • 兼容性好,几乎所有的浏览器都支持。
  • 抛开 HTML5,是 JSONP 很好的替代方案,比 JSONP 安全。

postMessage

window.postMessage(message,targetOrigin) 方法是 HTML5 新引进的特性,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源。监听 message 事件,事件能够获取三个重要的值。

  • source:发送消息的 window 对象。
  • data:发送的消息数据。
  • origin:调用 postMessage 方法发送消息的 window 的源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<iframe src="http://192.168.124.177:8081/b.html" style="display: none;"></iframe>
</body>
<script>
window.addEventListener('load', function () {
const origin = 'http://192.168.124.177:8081';
// 通过 iframe 向 http://192.168.124.177:8081/b.html 发送消息
window.frames[0].postMessage('我是 a,我发消息给了 b', origin);
}, false);

window.addEventListener("message", function (e) {
console.log('a.html 接收到了消息:' + e.data);
}, false)
</script>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>b.html</title>
</head>
<body></body>
<script>
window.addEventListener('message', function (e) {
if (e.source == window.parent) {
console.log('b.html 接收到了消息:' + e.data);
// 再使用 iframe 向父窗口发送消息,e.origin 为调用 postMessage 的 window 的源
parent.postMessage('我是 b,我发消息给了 a', e.origin);
}
}, false)
</script>
</html>

postMessage

参考

HTTP 访问控制(CORS)

构建 public APIs 与 CORS