浏览器跨域解决方法

目录

  1. 跨域概念解析
  2. JSONP解决方案
  3. document.domain + iframe方案
  4. location.hash + iframe方案
  5. window.name + iframe方案
  6. postMessage API方案
  7. CORS跨域方案
  8. WebSocket协议方案

一、跨域概念解析

当前浏览器的内容访问要求同源,所谓同源即是url的方案(协议)主机(域名)和端定义
只有当方案,主机和端口都匹配时,两个对象具有相同的起源
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介

二、JSONP解决方案

在html标签中很多带src属性的标签都可以跨域请求内容,比如我们熟悉的img图片标签。同理,script标签也可以,
通过<script>标签来实现跨域数据请求的方法。其工作原理主要依赖于浏览器对<script>标签的src属性没有同源策略限制这一特性。下面详细解释其实现跨域的步骤:

  1. 创建回调函数:在你的网页中定义一个全局函数,这个函数将被用来处理从其他域返回的数据。

  2. 构造<script>标签:动态地创建一个<script>元素,并设置它的src属性为目标服务器的URL。在这个URL中,你需要指定一个参数来告诉服务器你的回调函数名称。

  3. 服务器响应:当目标服务器收到请求后,它会构建一段JavaScript代码作为响应。这段代码调用你在第一步中定义的回调函数,并将需要的数据作为参数传递给这个函数。

  4. 执行脚本:浏览器接收到服务器的响应后,会执行这段JavaScript代码,从而触发你之前定义的回调函数,完成数据的获取和处理。

例如,假设你想从example.com获取数据,并且已经定义了一个名为handleResponse的回调函数,那么你可以创建一个<script>标签,其src属性为http://example.com/data?callback=handleResponse。服务器应该返回类似handleResponse({...})的JavaScript代码,其中{...}是实际的数据。

需要注意的是,由于JSONP的实现方式,它只能支持GET请求,而且如果服务器不正确地处理回调参数,可能会引发安全问题,如XSS攻击。因此,在使用JSONP时需要格外小心。随着CORS(跨源资源共享)机制的普及,JSONP逐渐被更为安全的CORS替代。

三、document.domain + iframe方案

使用 document.domain<iframe> 实现跨域通信是一种传统的方法,主要用于主域名相同但子域名不同的情况下。例如,父页面位于 a.example.com,而子页面位于 b.example.com。通过设置 document.domain,可以让这两个不同子域名的页面认为它们属于同一个源(即相同的主域名),从而允许它们之间进行直接的JavaScript交互。

实现步骤

  1. 设置 document.domain

    • 在父页面和子页面中都显式地设置 document.domain 为相同的主域名。比如,如果两个页面分别位于 a.example.comb.example.com,则在每个页面中添加以下代码:
      1
      document.domain = "example.com";
      这样做之后,两个页面就会被视为来自同一源。
  2. 建立 iframe 嵌套

    • 在父页面中嵌入一个指向子页面的 <iframe> 标签:
      1
      <iframe id="myIframe" src="http://b.example.com/page.html"></iframe>
  3. 实现通信

    • 在父页面中,可以通过获取 iframe 的 contentWindow 对象来访问子页面中的 JavaScript 变量或调用其函数。
      1
      2
      3
      4
      var iframe = document.getElementById('myIframe');
      var iframeWindow = iframe.contentWindow;
      // 调用子页面中的函数
      iframeWindow.someFunction();
    • 同样,在子页面中也可以访问父页面的 JavaScript 变量或调用其函数:
      1
      window.parent.someParentFunction();

注意事项

  • 这种方法仅适用于主域名相同的情况。如果主域名不同,则无法通过此方法解决跨域问题。
  • 设置 document.domain 可能会影响页面的安全性,因此应谨慎使用,并确保只在受信任的上下文中操作。

四、location.hash + iframe方案

使用 location.hash<iframe> 实现跨域通信是一种巧妙的方法,特别适用于需要在不同源的页面之间传递少量数据的情况。这种方法利用了 iframe 的 src 属性和 location 对象的 hash 属性(即 URL 中 “#” 后面的部分),允许父页面和嵌入的 iframe 页面之间进行单向或双向的数据交换。

实现原理

  • 父页面通过修改 iframe 的 src 属性中的 hash 部分来向 iframe 发送消息。
  • **子页面(iframe)**可以通过监听自身的 hashchange 事件来接收来自父页面的消息。
  • 类似地,子页面也可以通过修改父页面的 location.hash 来发送消息给父页面,父页面则监听自身的 hashchange 事件来接收这些消息。

实现步骤

父页面向 iframe 发送消息

  1. 在父页面中创建一个指向目标域名的 iframe:

    1
    <iframe id="myIframe" src="http://otherdomain.com/page.html"></iframe>
  2. 通过 JavaScript 修改 iframe 的 src 属性中的 hash 部分来发送消息:

    1
    2
    var iframe = document.getElementById('myIframe');
    iframe.src = iframe.src.split('#')[0] + '#' + encodeURIComponent('Hello from parent!');

iframe 接收消息并处理

  1. 在 iframe 内部监听 hashchange 事件来捕获来自父页面的消息:
    1
    2
    3
    4
    window.addEventListener('hashchange', function() {
    var message = decodeURIComponent(location.hash.substr(1));
    console.log('Received message:', message);
    });

iframe 向父页面发送消息

  1. 在 iframe 内部通过修改父页面的 location.hash 来发送消息:

    1
    2
    3
    4
    5
    try {
    parent.location.hash = 'Message from iframe';
    } catch (e) {
    // 跨域时会抛出异常,可以忽略或者做其他处理
    }
  2. 在父页面中监听 hashchange 事件以接收来自 iframe 的消息:

    1
    2
    3
    4
    window.addEventListener('hashchange', function() {
    var message = decodeURIComponent(location.hash.substr(1));
    console.log('Received message from iframe:', message);
    });

注意事项

  • 使用 location.hash 进行跨域通信有一定的局限性,比如只能传输简单的字符串信息,并且长度受限于浏览器对 URL 总长度的限制。
  • 由于涉及到直接操作 URL,频繁的更改可能会导致用户体验问题,如浏览器历史记录条目的增加。

五、window.name + iframe方案

使用 window.name<iframe> 实现跨域通信是一种较为巧妙的方法,利用了浏览器的一个特性:window.name 属性在页面的整个生命周期内保持不变,即使页面的 URL 发生变化,只要没有重新加载或导航到其他页面,window.name 的值就不会改变。这使得它成为一种可行的跨域通信手段。

工作原理

  1. 初始设置

    • 在父页面中创建一个指向目标域名的 iframe。
    • 目标页面(即 iframe 中加载的页面)设置其 window.name 为需要传递的数据。
  2. 数据传递

    • 父页面通过将 iframe 的 src 更改为与自身同源的一个空白页(例如,父页面所在域下的一个空 HTML 文件),然后访问 iframe 的 contentWindow.name 来读取之前设置的 window.name 值。
  3. 清理

    • 由于 window.name 可能会被后续操作覆盖或污染,通常会在读取完数据后立即将其重置为空字符串或其他默认值。

实现步骤

目标页面(子页面)

  1. 在目标页面(即要从其获取数据的页面)中设置 window.name
    1
    window.name = JSON.stringify({key: 'value'}); // 将需要传递的数据序列化为字符串

父页面

  1. 创建 iframe 并设置其 src 属性为目标页面的 URL:

    1
    <iframe id="myIframe" src="http://otherdomain.com/page.html" style="display:none;"></iframe>
  2. 使用 JavaScript 修改 iframe 的 src 属性为与父页面同源的一个空白页,并读取 window.name

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var iframe = document.getElementById('myIframe');

    // 当 iframe 加载完成时
    iframe.onload = function() {
    // 修改 iframe 的 src 到同源的一个空白页
    iframe.src = "about:blank"; // 或者是一个同源的空白HTML文件URL

    // 访问 iframe 的 window.name 获取数据
    var data = iframe.contentWindow.name;

    // 处理接收到的数据
    console.log(JSON.parse(data)); // 解析返回的数据

    // 清理 window.name 防止泄露
    iframe.contentWindow.name = '';
    };

注意事项

  • 这种方法适用于一次性的数据传递,因为一旦页面导航或重新加载,window.name 的值可能会被覆盖或丢失。
  • window.name 的大小有限制(不同浏览器可能有所不同),因此不适合传输大量数据。

六、postMessage API方案

postMessage 是一种用于实现安全的跨文档消息传递的API,允许来自不同源的窗口或iframe之间进行通信。它不仅适用于父页面和子iframe之间的通信,也适用于任何两个窗口(如通过 window.open 打开的新窗口)之间的通信。这种方法比使用 location.hashdocument.domainwindow.name 更加灵活和安全。

使用 postMessage 实现跨域通信

基本语法

发送消息的一方使用如下格式调用 postMessage 方法:

1
otherWindow.postMessage(message, targetOrigin, [transfer]);
  • message: 需要传递的数据。可以是任意类型的对象,但如果需要支持旧版浏览器,建议使用基本数据类型或JSON可序列化的对象。
  • targetOrigin: 指定目标窗口的源(协议+域名+端口),只有当目标窗口的源与此参数匹配时,才会发送消息。使用 "*" 表示不检查源,但这通常不推荐因为存在安全隐患。
  • [transfer] (可选): 一个数组,包含要转移所有权的对象(例如ArrayBuffer对象)。这些对象在发送后将不再属于发送者。

接收消息的一方需要监听 message 事件:

1
2
3
4
5
6
7
8
9
10
window.addEventListener('message', function(event) {
// 检查消息来源是否可信
if (event.origin !== 'http://expected-origin.com') return;

// 处理接收到的消息
console.log('Received message:', event.data);

// 如果需要回复,可以使用 event.source.postMessage()
event.source.postMessage('Response message', event.origin);
});

示例

父页面向 iframe 发送消息

父页面代码:

1
2
3
4
5
6
7
8
9
<iframe id="myIframe" src="http://otherdomain.com/page.html"></iframe>
<script>
var iframe = document.getElementById('myIframe');

// 向iframe发送消息
iframe.onload = function() {
iframe.contentWindow.postMessage('Hello from parent', 'http://otherdomain.com');
};
</script>

iframe代码:

1
2
3
4
5
6
7
8
9
10
// 监听来自父页面的消息
window.addEventListener('message', function(event) {
// 确认消息来源
if (event.origin !== 'http://parentdomain.com') return;

console.log('Message received from parent:', event.data);

// 回复消息给父页面
event.source.postMessage('Hello back to parent', event.origin);
});

iframe 向父页面发送消息

只需调整上述示例中的角色,即在 iframe 中使用 parent.postMessage() 发送消息,在父页面中监听 message 事件即可。

iframe代码:

1
2
// 向父页面发送消息
parent.postMessage('Hello from iframe', 'http://parentdomain.com');

父页面代码:

1
2
3
4
5
6
// 监听来自iframe的消息
window.addEventListener('message', function(event) {
if (event.origin !== 'http://otherdomain.com') return;

console.log('Message received from iframe:', event.data);
});

注意事项

  • 安全性: 始终验证 event.origin 来确保消息是从预期的来源接收的。不要信任未经验证的消息内容。
  • 数据类型: 虽然可以发送复杂对象,但要注意兼容性问题,特别是在处理旧版浏览器时。
  • 性能: 对于频繁的消息传递,考虑消息的大小和频率以避免影响性能。

七、CORS跨域方案

跨域资源共享(Cross-Origin Resource Sharing,简称 CORS)是一种基于HTTP的协议,它允许服务器声明哪些源可以访问其资源。CORS通过在HTTP响应中添加特定的头部信息来控制跨域请求的行为,从而增强了Web应用的安全性。

CORS的工作原理

CORS机制主要依赖于以下几个HTTP头部:

  • Origin: 这个头部由浏览器自动添加到跨域请求中,表明发起请求的来源(协议+域名+端口)。
  • Access-Control-Allow-Origin: 服务器使用这个头部来指定哪些源可以访问资源。它可以设置为具体的源(如http://example.com),也可以设置为通配符*表示允许所有源访问。
  • Access-Control-Allow-Methods: 指定允许的HTTP方法(如GET, POST, PUT等)。
  • Access-Control-Allow-Headers: 指定除了默认的简单请求头外,还允许哪些自定义请求头。
  • Access-Control-Allow-Credentials: 表示是否允许发送凭据(如Cookies、HTTP认证信息)。默认情况下,CORS请求不会发送这些凭据。

请求类型

CORS请求分为两种类型:简单请求预检请求

简单请求

满足以下条件的请求被认为是简单请求:

  • 使用GET, HEAD, 或 POST 方法。
  • HTTP 头部仅包含Accept, Accept-Language, Content-Language, Content-Type(但限制为application/x-www-form-urlencoded, multipart/form-data, 或 text/plain)。

对于简单请求,浏览器直接发送请求,并检查响应中的Access-Control-Allow-Origin头部以确定是否允许该请求。

预检请求

如果请求不符合简单请求的标准,浏览器会首先发送一个OPTIONS请求(称为预检请求)来询问服务器是否允许实际请求。预检请求会携带如下头部:

  • Access-Control-Request-Method: 实际请求将使用的HTTP方法。
  • Access-Control-Request-Headers: 实际请求将携带的额外HTTP头部。

服务器需要响应一个200状态码,并且在响应头部中包含必要的CORS头部信息。

注意事项

  • 安全性: 不要随意设置Access-Control-Allow-Origin*,尤其是在需要发送凭据的情况下。应该根据实际情况精确控制允许访问的源。

八、WebSocket协议方案

WebSocket 协议本身并不受限于同源策略(Same-Origin Policy),这意味着 WebSocket 连接可以在不同的源之间建立,而不需要像传统的 HTTP 请求那样处理跨域资源共享(CORS)。然而,在实际应用中,为了确保安全性,服务器通常会验证请求的来源,并决定是否接受连接。

WebSocket 跨域的基本原理

在 WebSocket 连接中,浏览器会在握手阶段发送一个标准的 HTTP 请求,该请求包含以下关键头部:

  • Origin: 浏览器自动添加此头部,指示发起 WebSocket 连接的源(协议+域名+端口)。
  • Sec-WebSocket-Key: 用于握手过程中的安全验证。
  • Sec-WebSocket-Version: 指示使用的 WebSocket 协议版本。

服务器在收到连接请求后,可以通过检查 Origin 头部来决定是否允许该连接。如果服务器认为该来源是可信的,则可以继续握手并建立连接;否则,服务器可以选择拒绝连接。

服务器端配置 WebSocket 跨域

以下是js服务器环境下的 WebSocket 跨域配置示例。

Node.js 使用 ws

在 Node.js 中使用 ws 库时,可以通过中间件或直接在 WebSocket 服务器上设置来处理跨域请求。

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
const WebSocket = require('ws');
const http = require('http');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

wss.on('connection', function connection(ws, req) {
const origin = req.headers['origin'];

// 验证 Origin 头部
if (origin === 'http://allowed-origin.com') {
console.log('Connection accepted from trusted origin');
ws.send('Welcome!');
} else {
console.log('Rejected connection from untrusted origin');
ws.close();
}

ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
});

server.listen(8080, () => {
console.log('WebSocket server is listening on port 8080');
});