本文内容:

除了常见跨域CORS、JSONP、WebSocket外的其他跨域方法

  • document.domain + iframe跨域
  • location.hash + iframe跨域
  • window.name + iframe跨域
  • postMessage跨域
  • nginx代理跨域
  • nodejs中间件代理跨域

document.domain + iframe跨域

要求:

主域相同,不同子域之间跨域

父窗口:http://www.website.com/a.html)

<iframe id="iframe" src="http://child.website.com/a.html"></iframe>
<script>
document.domain = 'website.com';
var user = 'admin';
</script>

子窗口:http://child.website.com/a.html)

<script>
document.domain = 'website.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>

location.hash + iframe跨域

具体实现:

a.html包含c.html,c.html包含b.html,最后b.html通过window.parent.parent修改a.html的location.hash

a.html,起在localhost:3000上

<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<iframe src="http://localhost:4000/c.html#yuhua"></iframe>
<script type="text/javascript">
//hash一变化,就获取变化后的hash值,这个hash值就是a传给c,c得到后响应,返回给b,b传递给a的
window.onhashchange = function(){
console.log(location.hash);
}
</script>
</body>
</html>

b.html,起在localhost:3000上

<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<!--
window 当前b的window
window.parent b的父级,也就是c
window.parent.parent b的爷爷,也就是c的爸爸,也就是a
-->
<script type="text/javascript">
window.parent.parent.location.hash = location.hash;
</script>
</body>
</html>

c.html,起在localhost:4000上

<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
let iframe = document.createElement("iframe");
iframe.src = 'http://localhost:3000/b.html#18';
document.body.appendChild(iframe);
</script>
</body>
</html>

思路:

  1. 访问a.html之后,就会加载c.html,同时把他自己的location.hash传给了c.html
  2. c.html加载后,会载入b.html,把自己的location.hash传给了b.html
  3. 这时候a.html与b.html是同源的,并且由于两层iframe嵌套,使用window.parent.parent即可把自己的从c.html收到的location.hash传到a.html

window.name + iframe跨域

window.name解释:

获取/设置窗口的名称。

语法:

string = window.name;
window.name = string;

image-20200328125639252

前提准备:

a.html,起在localhost:3000上

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>a.html</title>
</head>
<body>
<iframe id="iframe" src="http://localhost:4000/c.html" frameborder="0" "load()"></iframe>
<script type="text/javascript">
let first = true;//第一次加载
function load(){
if(first){
let iframe = document.querySelector("#iframe");
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
console.log(iframe.contentWindow.name);//yuhua
destoryFrame();//销毁iframe
}
}
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
</script>
</body>
</html>

b.html,起在localhost:3000上

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
</body>
</html>

c.html,起在localhost:4000上

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
c页面
<script type="text/javascript">
window.name = 'yuhua';
</script>
</body>
</html>

原理分析:

  1. 每个窗口都自己独立的window.name
  2. 当一个页面载入多个页面时,共享同一个window.name,同时都可以读写
  3. 所以说实际上就是,a.html加载c.html后,修改了iframe.src指向了我们同域的b.html,由于window.name是共享的,所以可以从b.html当中读到c.html中设置的window.name,这里b.html充当一个中间媒介(工具人

postMessage跨域

window.postMessage解释:

window.postMessage() 方法可以安全地实现跨源通信。

通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。

window.postMessage()方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

语法:

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

  • otherWindow
    其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索window.frames。

  • message
    将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。

  • targetOrigin
    通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*“(表示无限制)或者一个URI。

    在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。

    这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的origin属性完全一致,来防止密码被恶意的第三方截获。

    如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是*

    不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。

  • transfer 可选
    是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

举例:

a.html:(http://www.domain1.com/a.html)

<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>

b.html:(http://www.domain2.com/b.html)

<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
//可以通过e.origin来判断来源
var data = JSON.parse(e.data);
if (data) {
data.number = 16;

// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>

message 的属性有:

  • data
    从其他 window 中传递过来的对象。

  • origin
    调用 postMessage 时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。例如 “https://example.org (隐含端口 443)”、“http://example.net (隐含端口 80)”、“http://example.com:8080”。请注意,这个origin不能保证是该窗口的当前或未来origin,因为postMessage被调用后可能被导航到不同的位置。

    当发送窗口包含 javascript:data: URL时,origin属性的值是加载URL的脚本的

  • source
    对发送消息的窗口对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信。

原理分析:

a.html通过加载不同域的b.html,通过postMessage方法发送数据,同时设置事件监听器来监听返回数据

b.html也同样设置事件监听器来监听来自a.html的数据,得到数据后通过window.parent.postMessage发送数据返回

安全性:

  • 如果您不希望从其他网站接收message,请不要为message事件添加任何事件侦听器。

  • 如果您确实希望从其他网站接收message,请始终使用origin和source属性验证发件人的身份。 任何窗口(包括例如http://evil.example.com)都可以向任何其他窗口发送消息,并且您不能保证未知发件人不会发送恶意消息。 但是,验证身份后,您仍然应该始终验证接收到的消息的语法。 否则,您信任只发送受信任邮件的网站中的安全漏洞可能会在您的网站中打开跨网站脚本漏洞。

  • 当您使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*。 恶意网站可以在您不知情的情况下更改窗口的位置,因此它可以拦截使用postMessage发送的数据。

nginx代理跨域

nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
add_header Access-Control-Allow-Origin *;
}

nginx反向代理接口跨域

实现思路:

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

Nginx配置:

#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;
}
}

前端代码:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

Nodejs后台:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));

// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});

res.write(JSON.stringify(params));
res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

nodejs中间件代理跨域

原理与Nginx一致,都是通过代理服务器实现数据的转发

前台与后台和Nginx一致

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
// 代理跨域目标接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,

// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},

// 修改响应信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

参考链接

前端常见跨域解决方案(全)

【9大跨域解决方案】location.hash解决跨域的原理

【9大跨域解决方案】postMessage解决跨域的原理

【9大跨域解决方案】postMessage解决跨域的原理

【9大跨域解决方案】window.name解决跨域的原理