跨域
首发于:2021-04-26
跨域CORS全称是"跨域资源共享"(Cross-origin resource sharing)是指一个域下的文档或脚本试图去请求另一个域下的资源。与跨域相对的就是同源,同源策略(SOP,Same Origin Policy)是指“协议+域名+端口”三者相同。如果没有同源策略,浏览器很容易受到 XSS、CSFR等攻击。
跨域需要浏览器和服务器同时支持,目前所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个跨域的通信过程,都是浏览器自动完成的,不需要用户参与。对于开发者来说跨域通信与同源的AJAX通信没有区别,代码完全一样。浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加请求,但用户不会有感觉。
因此,实现跨域通信的关键是服务器,只要服务器实现了跨域,就可以跨域通信。
实现跨域的几种方案
JSONP
JSONP 就是利用 <script> 标签没有跨域限制的“漏洞”(这是历史遗留问题)来达到与第三方通讯的目的。JSONP的缺点就是只能实现 get 一种请求。
Nodejs 代码
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
const params = querystring.parse(req.url.split('?')[1]);
const cb = params.callback;
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(cb + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen(8888);index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function handleCb(res) {
console.log(res); // {param1: "123", param2: "456", callback: "handleCb"}
}
</script>
<script src="http://localhost:8888/test?param1=123¶m2=456&callback=handleCb"></script>
</body>
</html>document.domain + iframe 跨域
此方案仅限主域相同,子域不同的跨域应用场景,为了在本地模拟一下这个场景,需要先使用 nginx 做一些配置:
# 先 配置一下系统(Mac)的 hosts
$ cd /etc
$ sudo vim host
# 给 hosts 中添加以下配置
127.0.0.1 www.domain.com
127.0.0.1 child.domain.com# 修改 nginx 配置,配置好好记得重载
server {
listen 80;
server_name www.domain.com;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 80;
server_name child.domain.com;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}如果使用这个软件会更方便一些 SwitchHosts,下载地址 https://swh.app ,另外还要管理好一些代理,比如Mac下,网络->高级->代理->忽略这些主机与域的代理设置。除此之外,记得重启浏览器,最好使用无痕模式。
创建一个 child.html,放到 nginx 的 www 目录下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
child.com
<script>
console.log(window.parent.user)
</script>
</body>
</html>创建一个 parent.html,放到 nginx 的 www 目录下,然后访问 http://www.domain.com/parent.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var user = 'admin';
</script>
<iframe src="http://child.domain.com/child.html"></iframe>
</body>
</html>此时,打开控制台,你会发现是会有报错的
然后我们进行一下修改,强制把两个页面的 document.domain 改成一样的
// 在两个 html 的 script 中都添加下面代码,就可以访问到数据了
// 如果不强制修改俩网址的 document.domain 分别为 www.domain.com child.domain.com
document.domain = 'domain.com';location.hash + iframe 跨域
a 网站与 b网站跨域,a 网站与 c 网站同域,三个页面之间利用 iframe 的 location.hash 传值,相同域之间直接使用 js 来通信。
a 网站嵌入 b 网站,b 网站嵌入 c 网站,c 网站与 a 网站同域,所以可以使用 window.parent.parent 获取到 a 网站的对象。这样 a 网站通过 hash 值的变化把数据传给了 b 网站,b 网站通过 hash 值变化把数据传给c 网站, c 网站可以把数据传给 a 网站,这就完成了两个跨域网站数据的双向传递。
hosts 配置
127.0.0.1 www.domain1.com
127.0.0.1 www.domain2.comnginx.conf 配置
...
server_name www.domain1.com;
...
server_name www.domain2.com;a 网站 a.html http://www.domain1.com/a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function () {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>
</body>
</html>b 网站 b.html http://www.domain2.com/b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>
</body>
</html>c 网站 c.html http://www.domain1.com/c.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
</body>
</html>效果如下所示:
window.name + iframe 跨域
Window.name 属性就是一个窗口的名称,name 值在不同页面甚至不同域名加载后依旧存在,并且可以支持最大 2MB 的大小。
首先加载跨域的 iframe b 页面,b 页面会对自身 window.name 进行设置,然后就该执行该 iframe 的 onlaod 事件了,第一次执行的时候,立马将 iframe 跳转到了 同域的代理网页上,这个代理网页加载完成后会第二次执行 iframe 的 onload 事件,因为此时的 iframe 里面是同域的代理网页,所以我们可以拿到 window.name, 拿到数据之后,把 iframe 进行销毁。
注意:如果 iframe 不是同域的,其 window.name 是拿不到的,会报错。
hosts 与 nginx.conf 的配置与前一章节相同。
a 网站 a.html http://www.domain1.com/a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var proxy = function (url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function () {
if (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
} else if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function (data) {
alert(data);
});
</script>
</body>
</html>同域的代理网页面 proxy.html http://www.domain1.com/proxy.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>b 网站 b.html http://www.domain2.com/b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
window.name = 'This is domain2 data!';
</script>
</body>
</html>效果如下图所示:
window.postMessage 跨域
window.postMessage() 方法可以安全地实现跨源通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件。传递给 window.postMessage() 的参数(比如 message )将通过消息事件对象暴露给接收消息的窗口。
window.postMessage() 方法一般来说跨域解决以下问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的 iframe 消息传递
- 上面三个场景的跨域数据传递
window.postMessage(data, origin)方法接受两个参数:
- data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
- origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
a 网站 a.html http://www.domain1.com/a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function () {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function (e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>
</body>
</html>b 网站 b.html http://www.domain2.com/b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
// 接收domain1的数据
window.addEventListener('message', function (e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>
</head>
<body>
domain2
</body>
</html>跨域资源共享(CORS)
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,请看后面章节。
前端代码 a 网站 a.html http://www.domain1.com/a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8888', false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
alert(xhr.responseText);
}
};
</script>
</body>
</html>后端 nodejs 代码
const http = require('http');
const server = http.createServer();
const qs = require('querystring');
server.on('request', function (req, res) {
let postData = '';
// 数据块接收中
req.addListener('data', function (chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function () {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
// 允许访问的域(协议+域名+端口),如果此项为 * 那么前端设置xhr.withCredentials = true 请求将失败
'Access-Control-Allow-Origin': 'http://www.domain1.com',
/*
* 此处设置的cookie还是domain2:8888的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2:8888中写入一次cookie认证,后面的跨域接口都能从domain2:8888中获取cookie,从而实现所有的接口都能跨域访问
*/
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com:8888;HttpOnly', // HttpOnly的作用是让js无法读取cookie
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen(8888);代理跨域
代理跨域使用 Nginx 来代理,也可以使用 nodejs 来代理,其原理是相同的。推荐使用 nginx 代理,简单高效。
nginx 配置解决 iconfont 跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
add_header Access-Control-Allow-Origin *;
}nginx 反向代理接口跨域
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8888; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
root html;
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;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}nodejs 代码
const http = require('http');
const server = http.createServer();
const qs = require('querystring');
server.on('request', function (req, res) {
let postData = '';
// 数据块接收中
req.addListener('data', function (chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function () {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly', // HttpOnly的作用是让js无法读取cookie
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen(8888);前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain1.com:81', false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
alert(xhr.responseText);
}
};
</script>
</body>
</html>WebSocket 协议跨域
WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。
Nodejs 代码
// npm i ws -S 安装
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
console.log('ws on 8080');
wss.on('connection', function (ws) {
ws.on('message', function (message) {
ws.send('server response: ' + message);
console.log('received: %s', message);
});
});前端代码 a 网站 a.html http://www.domain1.com/a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var ws = new WebSocket('ws://127.0.0.1:8080');
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
ws.onmessage = function (evt) {
console.log("Received Message: " + evt.data);
ws.close();
};
ws.onclose = function (evt) {
console.log("Connection closed.");
};
</script>
</body>
</html>