HTML 跨域相关

本文的主要目的是简单介绍跨域问题以及相关的解决方案。

相关概念

跨域

跨域,指的是浏览器不能通过JS进行不同域的数据传输或通讯。它是由浏览器的同源策略造成的,是浏览器对Javascript施加的安全限制。

同源策略

同源策略, 是指浏览器通过JS请求的数据的协议端口相同。

注意:

  • 顶级域名和二级域名是不同域的,www.a.com 和 test.a.com 是不同域的

  • 127.0.0.1 和 localhost 虽然都是指向本地,但是他们也是不同域的

举些例子:

不跨域:

URL 说明 是否可以访问
http://www.a.com/1.js
http://www.a.com/2.js
同一域名相同端口的不同文件 能访问
http://www.a.com/dir1/1.js
http://www.a.com/dir2/2.js
同一域名相同端口的不同目录不同文件 能访问

跨域:

URL 说明 是否可以访问
http://www.a.com/1.js
https://www.a.com/2.js
协议不同 不能访问
http://www.a.com/1.js
http://www.b.com/2.js
域不同 不能访问
http://www.a.com/1.js
http://www.a.com:8080/2.js
端口不同 不能访问

解决方案

解决跨域问题的方案有以下7种方案,其中包含三种常用方案和四中小技巧

常用方案:

  • JSONP
  • CORS
  • 服务端代理

小技巧:

  • location.hash
  • window.name
  • postMessage
  • document.domain

1. JSONP

JSONP,全称是JSON with padding, 在Ajax中,用于解决老版本浏览器的跨域数据访问。

原理:

利用web浏览器加载js不受同源策略的影响,通过 script 标签进行跨域请求。

具体过程如下:

  1. 客户端设置好回调的函数,并将处理函数的函数名作为参数传递给服务器。
  2. 服务器接收到请求后,获取到回调函数的名称,并将处理后的数据放在回调函数的参数中,一起返回给客户端。
  3. 客户端接收到数据后,通过script标签,运行接收到的数据(调用本地处理函数)。

例子:

服务端:(Node 端口 3000)

1
2
3
4
5
6
7
8
9
10
11
12
13
let http = require('http')
let url = require('url')
http.createServer((req, res) => {
let data = {value: 'This is a value from Service'}
// 拿到回调函数名
let callback = url.parse(req.url, true) && url.parse(req.url, true).query && url.parse(req.url, true).query.callback
res.writeHead(200);
// 组装数据,并返回
res.end(`${callback}(${JSON.stringify(data)})`);
}).listen(3000)
console.log('服务器启动')

客户端:(端口80)

1
2
3
4
5
6
7
8
<!-- 1. 设置回调函数 -->
<script>
function _callback(data) {
alert('JSONP模拟测试:' + data.value);
}
</script>
<!-- 2.通过script标签请求接口,将回调参数提交给服务器 -->
<script src="http://127.0.0.1:3000?callback=_callback"></script>

优劣分析:

优点:

  1. 不受同源策略的影响,在老版本的浏览器上兼容性好。
  2. 无依赖,不需要其他( XMLHttpRequest 或 ActiveX )支持。

缺点:

  1. 只支持GET请求。
  2. 不能解决两个页面或iFrame之间的通信问题。
  3. 无法捕获 Jsonp 请求时的连接异常,只能通过超时进行处理

参考:

【原创】说说JSON和JSONP,也许你会豁然开朗,含jQuery用例 - 随它去吧 - 博客园

jsonp详解 - yuzhongwusan - 博客园

2. CORS

CORS,全称 跨域资源共享(Cross-origin resource sharing), 是W3C的一个新标准,他允许浏览器向跨源服务器,发出 XMLHttpRequest 请求。

CORS 需要浏览器和服务器同时支持才可以生效

1
2
3
4
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
res.header('Access-Control-Allow-Headers', 'X-Requested-With')
res.header('Access-Control-Allow-Headers', 'Content-Type')

其中 Access-Control-Allow-Origin 必须与客户端的 Origin 一致,否则将会跨域失效。* 表示接收来自任何域的请求。

优劣分析:

优点:

  1. 更加方便、安全
  2. 支持所有的HTTP请求类型

缺点:

  1. 是新的W3C标准,老浏览器不兼容(IE10 以上)

3. 服务端代理

跨域的请求通过后端代理,由后端代理代为请求,再将请求结果发送给客户端。

例子:Ngnix

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 28082;
server_name 127.0.0.1;
location / {
root html/lightning-wallet;
index index.html index.htm;
}
location /wallet {
proxy_pass http://127.0.0.1:2121;
}
}

webpack-dev 中的 proxyTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dev: {
env: require('./dev.env'),
port: 28082,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/wallet': {
target: 'http://192.168.1.124:18082',
changeOrigin: true,
pathRewrite: {
'^/wallet': '/wallet'
}
}
},
cssSourceMap: false
}

4. location.hash

原理: 利用改变地址中的hash值不会导致页面刷新。通过改变hash值来传递数据,但是传递的数据量有限。

说明:

域1: 页面A、页面C(代理页)

域2: 页面B(数据接口所在的域)

具体流程:

  1. 页面A 通过js动态添加 iframe
  2. iframe 加载 页面B, 页面B 获取组装数据。
  3. 页面B 获取数据后,将数据作为 页面C 的hash值,跳转到 页面C
  4. 页面C 再通过修改 parent.location.hash 来将数据传递到 页面A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 页面A -->
<script>
let ifr = document.createElement('iframe')
ifr.style.display = 'none'
ifr.src = "http://localhost:8081/data.html#data"
document.body.appendChild(ifr)
function checkHash() {
try {
let data = location.hash ? location.hash.substring(1) : ''
console.log('获得到的数据是:', data);
}catch(e) {
}
}
window.addEventListener('hashchange', function(e) {
console.log('获得的数据是:', location.hash.substring(1))
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 页面B -->
<script>
switch(location.hash) {
case "#data":
callback()
break
}
function callback() {
const data = "data.html 的数据"
try {
parent.location.hash = data
}catch(e) {
// ie, chrome 下的安全机制无法修改 parent.location.hash
// 所以要利用一个中间的代理 iframe
var ifrproxy = document.createElement('iframe')
ifrproxy.style.display = 'none'
ifrproxy.src = 'http://localhost:8080/proxy.html#' + data
document.body.appendChild(ifrproxy)
}
}
</script>

缺点:

  1. 数据直接暴露在了 url 中
  2. 数据容量和类型都有限等等

5. window.name

window.name 是当前窗口的名字, iframe 通样有 window.name 属性。

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

  1. 页面A 通过js动态添加 iframe
  2. iframe 加载 页面B, 并设置 onload 函数,取到数据后跳转到 页面C,同时重新设置 onload 函数读取数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 页面A.html -->
<script>
let data = ''
const ifr = document.createElement('iframe')
ifr.src = "http://localhost:8081/data.html" // 页面B
ifr.style.display = 'none'
document.body.appendChild(ifr)
ifr.onload = function() {
ifr.onload = function() {
data = ifr.contentWindow.name
console.log('收到数据:', data)
}
ifr.src = "http://localhost:8080/proxy.html" // 页面C
}
</script>
1
2
3
4
<!-- 页面B.html -->
<script>
window.name = "data.html 的数据!";
</script>

6. postMessage

postMessage HTML5 新增的一项功能,跨文档消息传输(Cross Document Messaging)

Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 都支持。

1
2
3
4
5
6
7
8
9
10
11
<!-- 页面A -->
<iframe src="http://localhost:8081/data.html" style='display: none;'></iframe>
<script>
window.onload = function() {
let targetOrigin = 'http://localhost:8081'
window.frames[0].postMessage('index.html 的 data!', targetOrigin)
}
window.addEventListener('message', function(e) {
console.log('index.html 接收到的消息:', e.data)
});
</script>
1
2
3
4
5
6
7
8
9
10
11
<!-- 页面B -->
<script>
window.addEventListener('message', function(e) {
if(e.source != window.parent) {
return
}
let data = e.data
console.log('data.html 接收到的消息:', data)
parent.postMessage('data.html 的 data!', e.origin)
})
</script>

7. document.domain

这种方式用于主域相同而子域不同的情况下。通过设置 document.domain来实现通讯。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- A页面 sub1.example.com -->
<script>
document.domain = 'example.com';
let ifr = document.createElement('iframe');
ifr.src = 'http://sub2.example.com/data.html';
ifr.style.display = 'none';
document.body.append(ifr);
ifr.onload = function() {
let win = ifr.contentWindow;
alert(win.data);
}
</script>
1
2
3
4
5
<!-- B页面 sub2.example.com -->
<script>
document.domain = 'example.com';
window.data = 'data.html 的数据!';
</script>