3

常用跨域方法总结

为什么要跨域?

因为浏览器的一种安全机制——同源策略的限制,导致不能直接获取不同源的资源,所以要跨域。

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

限制之一是不能通过ajax的方法去请求不同源中的文档。
第二个限制是浏览器中不同域的框架(iframe)之间是不能进行js的交互操作的。

那么什么才叫“同源”呢?

  • 协议相同
  • 域名相同
  • 端口号相同

clipboard.png

图来自MDN,参见最后Reference.

下面介绍常用的几种跨域方法。

jsonp 跨域

原理:

  • 利用了<script>标签不受浏览器同源限制的影响,
  • 因为借用<script>发起的请求,所以会把请求到的内容当作js代码来解析执行。

缺点:只能发送get请求
下面是一个简单的例子

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
function callback (msg) {
  console.log('json:', msg)
}
</script>
<script src="http://localhost:8080?cb=callback ">
</script>
</body>
</html>

如上,定义了一个全局的callback 函数,然后利用<script>来发起get请求,注意把函数名称callback 一同发送给服务端,为什么呢,接着往下看服务端代码

//nodeJs
var http = require('http');
var url = require('url');

/**
 * 客户端请求的函数
 * @param req
 * @param res
 */
function onRequest (req, res) {
  console.log("获取到的请求参数的路径:" + req.url);

  //得到键值对
  var arg = url.parse(req.url, true).query;
  //打印键值对中的值
  console.log(arg.cb);

  res.writeHead(200);
  res.write(arg.cb + '("我今天要用jsonp来跨域获取数据")');
  res.end();
}

http.createServer(onRequest).listen(8080);

服务端简单解析cb参数(我们传的函数名称),然后返回一段字符串callback("我今天要用jsonp来跨域获取数据")
浏览器会收到响应如下:

clipboard.png

clipboard.png

发现什么了吗?响应内容callback("我今天要用jsonp来跨域获取数据")会被当作js代码来执行,正好调用了我们之前定义的callback函数。
由此,我们成功的利用jsonp通过跨域获取到了想要的数据。

document.domain跨域(子域名不同的框架之间)

开头我们说到不同源的框架之间是不能进行js交互操作的,其实是可以获取window对象,但不能获取window的属性。
原理:

  • document.domain的值是可以设置的,但仅限于设置为自身或是更高一级的父级域名(主域名相同)。

那么主域名相同,子域名不同的框架之间跨域获取数据的思路就来了,我们把它们的document.domain都设置成主域名不就完事了?
比如有一个页面a.google.com/1.html
这里参考了其他文章的代码。出处见本文最下方

<iframe id = "iframe" src="http://b.google.com/2.html" onload = "test()"></iframe>
<script type="text/javascript">
    document.domain = 'google.com';//设置成主域
    function test(){
        alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
    }
</script>

另一个页面b.google.com/2.html只需设置主域名为google.com,两个框架间就可以进行交互啦。

<script type="text/javascript">
document.domain = 'google.com';//设置成主域
</script>

location.hash跨域(不同源的框架之间)

原理:

  • hash字段(经常用于锚点定位)不属于http协议的部分,请求不会携带hash信息,所以改变不会重新请求资源(但是会产生新的浏览器历史记录,许多前端路由也是借用这个原理实现的)
  • 父窗口可以对iframe进行URL读写,iframe也可以读写父窗口的URL(不同源的话,IE、Chrome不允许修改parent.location.hash的值,但我们仍有处理方式)

思路:

  • 如果是父窗口向子窗口跨域传递数据,直接修改子窗口url的hash就可以了,比较简单这里就不贴代码了。
  • 子窗口向父亲窗口跨域传递数据就需要多加一个代理窗口(因为IE,Chrome),这个代理窗口和父亲窗口同源就可以了,在这个代理窗口改变父亲窗口的hash,父亲窗口监听hashchange就可以了。

代码如下:
父亲窗口页面
http://localhost:63342/test-field/cross-origin/local-test/hash-parent.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<iframe src="http://blank121.github.io/test-field/cross-origin/hash.html">
</iframe>
<script>
console.log('old hash:', location.hash)
window.addEventListener('hashchange', function (ev) {
  console.log('new hash:', location.hash)
})
</script>
</body>
</html>

子窗口页面
http://blank121.github.io/tes...

try{
  parent.location.hash='今天我要用hash跨域'
  //chrome ie 直接修改parent.location.hash报错
}catch (e){
  var iframe = document.createElement('iframe')
  iframe.src = 'https://localhost:63342/test-field/cross-origin/local-test/hash-proxy.html'+'#今天我要用hash跨域'
  document.body.appendChild(iframe);
}

代理窗口页面
http://localhost:63342/test-field/cross-origin/local-test/hash-proxy.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
console.log('proxy',location.hash)
parent.parent.location.hash = location.hash
</script>
</body>
</html>

如上,在子窗口页面内修改了代理窗口的hash值,代理窗口又修改了父亲窗口的hash值,父亲窗口监听hashchange就可以获取到不同源的子窗口传来的数据啦。
控制台结果如下:

clipboard.png

window.name跨域(不同源框架间)

原理:

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

利用一个窗口的生命周期内,载入不同页面window.name不变的特性
贴代码前首先去做个有趣的实验
在必应的首页设置一下window.name

clipboard.png

然后输入location.href = 'http://www.baidu.com',跳转到百度后再看一下name

clipboard.png

666,window.name果然没骗我,没有变化。

接下来先贴代码
父亲窗口:
http://localhost:63342/test-field/cross-origin/local-test/name-parent.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<iframe id="frame" src="https://blank121.github.io/test-field/cross-origin/name.html"></iframe>
<script>
var iframe = document.getElementById('frame')
var loaded = false
iframe.onload = function () {
  if (!loaded) {
    loaded = true
    iframe.src = 'http://localhost:63342/test-field/cross-origin/local-test/name-proxy.html'
  } else {
    console.log(iframe.contentWindow.name)
  }
}
</script>
</body>
</html>

子窗口:
https://blank121.github.io/te...

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>window.name跨域</title>
</head>
<body>
<script>
window.name='今天,我要用window.name来实现跨域'
</script>
</body>
</html>

代理窗口(啥也没做。主要是要和父亲窗口同源来传递name)

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

代码如上,主要思路就是利用window.name不变性,子窗口设置了name之后,来加载一个和父亲窗口同源的代理窗口,以此来获取name(注意子窗口和代理窗口是同一个iframe,加载不同页面而已,所以window.name不变)

clipboard.png

postMessage跨域

首先了解一下postMessage
otherWindow.postMessage(message, targetOrigin);
otherWindow:指目标窗口,也就是给哪个window发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口
message: 是要发送的消息,类型为 String、Object (IE8、9 不支持)
targetOrigin: 是限定消息接收范围,不限制请使用 *

postMessage是HTML5新特性,跨域简直太方便了,就是兼容性要注意一下。

发送方页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<iframe id="frame" src="https://blank121.github.io/test-field/cross-origin/receive.html">
</iframe>
<script>
var iframe = document.getElementById('frame')
iframe.onload = function () {
    iframe.contentWindow.postMessage("我今天就是要用postMessage跨域","https://blank121.github.io")
}
</script>
</body>
</html>

接收方页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
function receiveMessage(event)
{
  var p = document.createElement('p')
  p.innerText = event.data
  document.body.appendChild(p)
}
window.addEventListener("message", receiveMessage, false);
</script>
</body>
</html>

接收方监听一下message事件,就可以接收到信息啦。

clipboard.png

WebSocket跨域

WebSocket 是一种在客户端与服务器之间保持TCP长连接的网络协议,这样它们就可以随时进行信息交换(双工通讯)。
虽然任何客户端或服务器上的应用都可以使用WebSocket,但原则上还是指浏览器与服务器之间使用。通过WebSocket,服务器可以直接向客户端发送数据,而无须客户端周期性的请求服务器,以动态更新数据内容。

WebSocket 非常强大,笔者在这方面也是小白级别的,以后有时间会详细研究学习。

跨域代码如下
页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
</body>
</html>
<script>
// Create WebSocket connection.
const socket = new WebSocket('ws://127.0.0.1:8080');

// Connection opened
socket.addEventListener('open', function (event) {
  socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
  console.log('Message from server', event.data);
});
</script>

服务端nodeJs代码:

var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('received: %s', message);
     ws.send("hello"+message);
  });
});

结果如图:
浏览器端:

clipboard.png

服务端:

clipboard.png

完美实现了跨域。

CORS(cross origin resource sharing 最常用)

老生常谈的CORS,优秀的文章已经非常多了,大家可以搜一下,非常重要,有机会我会专门写一篇文章来学习总结,在此就不再详述了

最后

不得不说,这些方法还是比较巧妙的,在此写下一篇文章来总结一下,感觉自己面对跨域丝毫不慌啦。

Reference

前端跨域整理
正确面对跨域,别慌
浏览器的同源策略


178096413
841 声望39 粉丝