代码视界

Hanpeng Chen的个人博客

一文掌握9大跨域解决方案

本文于 1200 天之前发表,文中内容可能已经过时。

什么是跨域

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源。通常我们讲的跨域,是由浏览器同源策略限制的一类请求场景。

同源策略

同源指的是两个URL的协议、域名和端口三者都相同,即使两个不同 的域名指向相同的IP地址,也非同源。

同源策略(SOP:Same origin policy)是浏览器的一套基础的安全策略制约,用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。

同源策略主要表现在DOM、web数据和网络三个层面。

DOM层面: 同源策略限制了来自不同源的JavaScript脚本对当前DOM对象读和写的操作。

数据层面: 同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。

网络层面: 同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。

跨域的解决方法

主要有以下九种解决方案:

  • JSONP
  • CORS(跨域资源共享,最常用)
  • postMessage + iframe
  • document.domain + iframe
  • window.name + iframe
  • location.hash + iframe
  • WebSocket
  • nginx代理跨域
  • nodejs中间件代理跨域

JSONP

浏览器只对XMLHttpRequest请求有同源请求限制,而对script标签src属性、link标签ref属性和img标签src属性没有这这种限制,利用这个“漏洞”就可以很好的解决跨域请求。JSONP就是利用了script标签无同源限制的特点来实现的。当然需要后端服务器的配合,返回一个合法的JS脚本,一般是一条调用js函数的语句,数据作为函数的入参。

我们通过下面的例子来简单展示如何通过JSONP来解决跨域。

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express');
const app = express();

app.get('/jsonp', (req, res) => {
let {wd, cb} = req.query;
console.log(wd, cb);
res.end(`${cb}('接口返回测试数据')`);
})

app.listen(3000, () => {
console.log('app listening on port 3000');
})
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
36
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jsonp解决跨域</title>
</head>
<body>
<script>
function jsonp({url, params, cb}) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
window[cb] = function(data) {
resolve(data)
document.body.removeChild(script);
}
params = {...params, cb};
let arrs = [];
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`;
document.body.appendChild(script);
})
}
jsonp({
url: 'http://localhost:3000/jsonp',
params: {
wd: 'b'
},
cb: 'show' // 回调函数名
}).then(data => {
console.log(data)
})
</script>
</body>
</html>

JSONP有以下几个缺点:

  • 只支持GET请求而不支持POST等其它类型的HTTP请求
  • 只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
  • JSONP在调用失败的时候不会返回各种HTTP状态码。
  • 不安全。万一提供jsonp的服务存在页面注入漏洞,容易遭受xss攻击。

CORS(跨域资源共享,最常用)

跨源资源共享 (CORS,Cross-origin resource sharing)是一种基于HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(域,协议和端口),这样浏览器可以访问加载这些资源。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

接下来我们通过一个简单的例子来一起看下CORS的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express');
const app = express();

let whiteList = ['http://localhost:3000']

app.use(function (req, res, next) {
console.log(req.headers);
let origin = req.headers.origin;
if (whiteList.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // 接受origin这个域名的请求
res.setHeader('Access-Control-Allow-Headers', 'x-name'); // 表明服务器支持的所有头信息字段
}
next();
})

app.get('/getData', (req, res) => {
res.end('接口返回测试数据');
})

app.use(express.static(__dirname));

app.listen(4000)

我们通过http://localhost:3000/index.html打开下面的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
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CORS</title>
</head>
<body>
<p>cors test html</p>

<script>
let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('x-name', 'test')
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
console.log(xhr.response)
}
}
}
xhr.send();
</script>
</body>
</html>

该方法主要是后端服务接口在响应报文中设置相应的正确CORS响应头。这也是目前最常用的解决跨域问题的方法。

更多详细内容可以查看阮一峰老师的文章:https://www.ruanyifeng.com/blog/2016/04/cors.html

postMessage + iframe

postMessage是H5引入的一个API,该方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

我们起两个服务,a.html在http://localhost:3000上,b.html在http://localhost:4000上。

两个HTML代码如下:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>a page</p>
<span id="message"></span>
<iframe src="http://localhost:4000/b.html" frameborder="1" id="frame"></iframe>

<script>
window.onload = function() {
let frame = document.getElementById('frame');
frame.contentWindow.postMessage('测试消息', 'http://localhost:4000/b.html')
}
window.onmessage = function (e) {
// 接受消息
document.getElementById('message').innerHTML = `收到${e.origin}的消息:${e.data}`;
}
</script>
</body>
</html>

b.html

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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>b page</p>
<span id="message"></span>

<script>
window.onmessage = function (e) {
// 接受消息
console.log('b page onmessage', e.data);
document.getElementById('message').innerHTML = `收到${e.origin}的消息:${e.data}`;
top.postMessage('b 页面收到消息', 'http://localhost:3000/a.html');
}
</script>
</body>
</html>

window.name

页面在浏览器端展示时,我们能拿到全局变量window,window变量有个name属性,该属性具有下面几个特征:

  • 每个窗口都有独立的window.name与之对应
  • 在一个窗口的生命周期中(被关闭前),窗口载入的所有页面同时共享一个window.name,每个页面对window.name都有读写的权限。
  • window.name一直存在于当前窗口,即使是有新的页面载入也不会改变window.name的值。
  • window.name可以存储不超过2M的数据,数据格式按需自定义

我们准备三个页面:a.html和b.html在http://localhost:3000上,c.html在http://localhost:4000上。

目标:要在a页面获取c页面发送的数据

思路:a先引用c,c把值放到window.name,把a的引用地址改到b

代码如下:
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
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>a page</p>
<iframe src="http://localhost:4000/c.html" frameborder="0" id="frame" onload="load()"></iframe>

<script>
let first = true;
function load() {
if (first) {
let frame = document.getElementById('frame');
frame.src = 'http://localhost:3000/b.html';
first = false;
} else {
console.log(frame.contentWindow.name);
}
}
</script>
</body>
</html>

b.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>b page</p>
</body>
</html>

c.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
window.name = 'window.name实现跨域'
</script>
</body>
</html>

location.hash

实现原理: a.html 欲与 c.html 跨域相互通信,通过中间页 b.html 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

我们准备三个页面:a.html和b.html在http://localhost:3000上,c.html在http://localhost:4000上。

目标:在a页面获取c页面发送的数据。

思路:a给c传一个hash值,c收到hash值后,c把hash值传递给b,b将结果放到a的hash值中

代码如下:

a.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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe src="http://localhost:4000/c.html#testData" frameborder="0" id="frame" onload="load()"></iframe>

<script>
window.onhashchange = function() {
console.log(location.hash);
}
</script>
</body>
</html>

b.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
window.parent.parent.location.hash = location.hash
</script>
</body>
</html>

c.html

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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#cPageToBData';
</script>
</body>
</html>

document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。

只需要给页面添加 document.domain =’test.com’ 表示二级域名都相同就可以实现跨域。

实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe src="http://b.test.com/b.html" frameborder="0" id="frame" onload="load()"></iframe>

<script>
document.domain = 'test.com'
function load() {
console.log(frame.contentWindow.data);
}
</script>
</body>
</html>

b.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
document.domain = 'test.com';
var data = '这是b页面的数据';
</script>
</body>
</html>

WebSocket

WebSocket是一种浏览器的API,它的目标是在一个单独的持久连接上提供全双工、双向通信。

WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

同源策略对WebSocket不适用。

我们来看个简单例子:本地socket.html向localhost:3000发送数据和接受数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// socket.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 高级API 不兼容, socket.io 库
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function() {
socket.send('test data');
}
socket.onmessage = function(e) {
console.log(e.data);
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
// server.js
let express = require('express');
let app = express();

let WebSocket = require('ws');
let wss = new WebSocket.Server({port:3000})
wss.on('connection', function(ws) {
ws.on('message', function(data) {
console.log(data);
ws.send('response data');
})
})

nodejs中间件代理跨域

同源策略针对的是浏览器,如果是服务器向服务器请求则无需遵循同源策略。nodejs中间件代理跨域就是利用这个原理,将跨域请求发给代理服务器,代理服务器去做请求转发。

代理服务器需要做以下几个步骤:

  • 接受客户端请求
  • 将请求转发给服务器
  • 拿到服务器响应数据
  • 将响应转发给客户端

我们来看下面的例子:本地index.html文件,通过代理服务器 localhost:3000 向目标服务器 localhost:4000 请求数据:

index.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
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>http proxy</title>
</head>
<body>
<p>http://localhost:3000/index.html</p>

<script>
let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/getData', true)
xhr.setRequestHeader('x-name', 'test')
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
console.log(xhr.response)
}
}
}
xhr.send();
</script>
</body>
</html>

proxyServer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

// 利用 Express 托管静态文件,可通过http://localhost:3000/index.html来访问index.html,实现跨域
app.use(express.static(__dirname));

// 代理服务器操作
// 设置允许跨域访问该服务
app.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', '*');
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});

// http-proxy-middleware
// 中间件 每个请求来之后 都会转发到 http://localhost:3001 后端服务器
app.use('/', createProxyMiddleware({target: 'http://localhost:4000', changeOrigin: true}))

app.listen(3000)

server.js

1
2
3
4
5
6
7
8
9
10
const express = require('express');
const app = express();

app.get('/getData', (req, res) => {
res.end('nodejs中间件代理跨域 返回数据');
})

app.use(express.static(__dirname));

app.listen(4000)

nginx

实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。

使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。

nginx的配置简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;

# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}

文中示例代码cross-domain

参考资料

https://www.jianshu.com/p/e1e2920dac95

https://www.ruanyifeng.com/blog/2016/04/cors.html

欢迎关注微信公众号: 『前端极客技术』『代码视界』
支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励