如何解决跨域的7个不同方法-有demo

261 阅读13分钟

一、什么是跨域

浏览器跨域问题是由浏览器的同源策略所引起的限制。同源策略要求浏览器只能访问与当前页面具有相同协议、域名和端口的资源,而对于不符合同源要求的跨域请求,浏览器会阻止其访问。(协议、域名、端口 有任一个不同,就是跨域)

二、为什么浏览器会禁止跨域

当浏览器默认不允许跨域时,主要基于以下安全考虑:

  1. 数据隐私保护:浏览器的同源策略确保不同域名之间的数据隔离,防止恶意网站通过跨域请求获取用户的敏感信息。
  2. CSRF(Cross-Site Request Forgery)攻击防护:同源策略可以有效防止跨站请求伪造攻击。浏览器限制跨域请求可以防止未经授权的网站发送恶意请求到其他网站
  3. 用户身份验证保护:浏览器默认不允许跨域读取其他域名下的 Cookie,确保用户身份验证信息不会被恶意网站获取。
  4. 访问控制限制:浏览器同源策略限制了对其他域名下的 DOM、JavaScript 对象和资源的访问,防止恶意网站操作其他域名的页面
  5. 沙箱环境保护:浏览器同源策略确保每个域名的 JavaScript 代码在沙箱环境中运行,防止恶意代码对其他域名的页面造成影响。
  6. 网络安全增强:跨域请求的限制减少了攻击者利用浏览器发送恶意请求的机会,提高了网络安全性。 这些安全考虑确保了用户的数据隐私和网络安全,同时防止恶意网站进行攻击和滥用。

三、如何解决跨域

1、使用 CORS(跨域资源共享)最常用

CORS 是一种基于 HTTP 头部的机制,允许服务器声明允许跨域请求的策略。在服务端设置合适的响应头,包括 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 等字段,来允许跨域请求。 浏览器会自动进行CORS通信,只要后端实现了CORS那么调用跨域接口就能成功。(服务器设置 Access-Control-Allow-Origin 就表示开启了CORS)

1.1、demo

下面是一个使用 CORS 进行跨域资源共享的示例,包括前端和后端的实现:

前端代码:

// 发送跨域请求
fetch('http://example.com/api/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json'
  },
  mode: 'cors' // 设置请求模式为 CORS
})
  .then(response => response.json())
  .then(data => {
    console.log('Response:', data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

后端代码:

const express = require('express');
const app = express();

// 设置 CORS 头部信息
app.use(function(req, res, next) {
  res.setHeader('Access-Control-Allow-Origin', 'http://example.com'); // 允许指定的域进行跨域访问,或设置为 '*' 允许任意域
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); // 允许的 HTTP 方法
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); // 允许的请求头
  res.setHeader('Access-Control-Allow-Credentials', 'true'); // 允许携带凭证信息(如 cookies)
  next();
});

// 处理跨域请求
app.get('/api/data', function(req, res) {
  const data = { message: 'Hello, CORS!' };
  res.json(data);
});

// 启动服务器
app.listen(3000, function() {
  console.log('Server is running on port 3000');
});

在上面的示例中,前端通过 fetch 函数发送跨域请求到后端的 /api/data 路径,后端通过设置相应的 CORS 头部信息,允许指定的域进行跨域访问。在前端的请求中,通过 mode: 'cors' 设置请求模式为 CORS。通过这样的配置,浏览器会在发送请求时自动发送一个OPTIONS 请求进行预检,然后再发送实际的 GET 请求获取响应数据。

需要注意的是,前端的请求域名(http://example.com)需要和后端的 CORS 头部信息中的 Access-Control-Allow-Origin 字段匹配,否则跨域请求仍然会被浏览器拦截。

关于 设置 CORS 头部信息有时间继续补充!

2、使用 JSONP

JSONP 是一种利用 <script> 标签可以跨域加载资源的特性,来实现跨域请求。具体操作要求是:

  • 在页面中动态创建 <script> 标签,并指定跨域的 URL
  • 服务端返回的响应需要是一个函数调用,将数据作为参数传递给回调函数。

2.1、demo

下面是一个使用 JSONP 的详细示例: 假设我们有两个域名,example.comapi.example.com,我们希望在 example.com 的页面中获取来自 api.example.com 的数据。

example.com 的页面中,我们可以创建一个 <script> 标签,并将其 src 属性设置为 api.example.com 的接口地址,同时在 URL 中传递一个回调函数的名称作为参数。

<!DOCTYPE html>
<html>
<head>
  <title>JSONP Demo</title>
</head>
<body>
  <h1>JSONP Demo</h1>
  <!-- 这个是 example.com 的页面 -->
  <script>
    // 定义回调函数,处理从服务器返回的数据
    function handleResponse(data) {
      console.log('Received data:', data);
      // 在这里处理从服务器返回的数据
    }
  </script>
  <!-- 核心依旧是 调用 /data接口,返回数据,只不过事先指定好了 回调函数名 -->
  <!-- 注意: callback只是一个约定的query名字,当然可以修改 -->
  <script src="http://api.example.com/data?callback=handleResponse"></script>
</body>
</html>

api.example.com服务器端,我们需要根据传入的回调函数名称,将数据包装在一个函数调用中返回给客户端。核心是设置 'Content-Type'='application/javascript'

// Express.js 示例
app.get('/data', (req, res) => {
  // 为了避免安全风险,服务器端应对回调函数名称进行校验和限制,以防止恶意代码注入攻击
  const callbackName = req.query.callback;
  const responseData = { message: 'Hello, JSONP!' };
  
  // 包装数据在回调函数中返回给客户端
  const responseString = `${callbackName}(${JSON.stringify(responseData)})`;
  // 设置响应头,指定内容类型为 JavaScript
  res.set('Content-Type', 'application/javascript');
  
  // 发送数据给客户端
  res.send(responseString);
});

当浏览器加载 example.com 的页面时,它会动态创建一个 <script> 标签,并将 src 设置为 http://api.example.com/data?callback=handleResponse。服务器返回的内容将被当作 JavaScript 代码执行,回调函数 handleResponse 将在服务器返回的数据被传递给它时被调用。

注意,JSONP只支持GET请求,并且需要服务器端对回调函数的名称进行验证和处理,以防止安全问题。

2.2、封装

function jsonp(url, callbackName, callback) {
  // 创建一个全局唯一的回调函数名称
  const callbackFuncName = `jsonp_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
  // 将回调函数名称作为参数添加到 URL 中
  // callbackName是给服务器标识用,callbackFuncName是浏览器执行用。
  const callbackUrl = `${url}?${callbackName}=${callbackFuncName}`;
  
  // 创建一个全局回调函数
  window[callbackFuncName] = function(data) {
    // 调用传入的回调函数处理返回的数据
    callback(data);
    // 请求完成后,删除全局回调函数和创建的 script 元素
    delete window[callbackFuncName];
    script.parentNode.removeChild(script);
    console.log(`${callbackFuncName}回调函数执行完毕`);
  };

  // 创建一个 script 元素,并设置其 src 属性为包含回调函数的 URL
  const script = document.createElement('script');
  script.src = callbackUrl;
  // 将 script 元素添加到页面中,触发跨域请求
  document.body.appendChild(script);
}

使用该封装函数可以进行 JSONP 请求,示例如下:

jsonp('http://api.example.com/data', 'handleResponse', function(data) {
  console.log('Received data:', data);
  // 在这里处理从服务器返回的数据
});

上述封装函数会动态生成一个全局唯一的回调函数名称,并将其作为参数添加到 URL 中。然后,创建一个 <script> 元素,设置其 src 属性为包含回调函数的 URL,并将其添加到页面中。当服务器返回响应时,会调用全局回调函数,从而触发传入的回调函数对返回的数据进行处理。处理完毕后,删除全局回调函数和创建的 <script> 元素,完成请求的清理工作。

3、使用 WebSocket

WebSocket 是一种基于 TCP 的全双工通信协议,它允许在客户端和服务器之间建立持久性的连接,实现实时的双向数据传输。由于 WebSocket 协议的存在(没有遵循同源策略),实际上不涉及跨域问题,因此不需要进行特殊的处理来解决跨域。

3.1、demo

服务端,创建一个 WebSocket 服务器,监听指定的端口,接受客户端的连接,并处理消息:connection、message、close

const WebSocket = require('ws');

// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ port: 8080 });
// 监听连接事件
wss.on('connection', function(ws) {
  console.log('Client connected');
  // 监听消息事件
  ws.on('message', function(message) {
    console.log('Received message:', message);
    // 向客户端发送消息
    ws.send('Hello, client!');
  });
  // 监听关闭事件
  ws.on('close', function() {
    console.log('Client disconnected');
  });
});

客户端,创建一个 WebSocket 连接到服务器,并进行通信: 其中 new WebSocket是浏览器自带的对象(不需要额外引入包),另外用WebSocket对象建立的 连接不受同源策略的限制,因此可以在不同的域之间进行通信。 监听的函数名为open、message、close

// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:8080');
// 监听连接事件
socket.addEventListener('open', function() {
  console.log('Connected to server');
  // 发送消息到服务器
  socket.send('Hello, server!');
});

// 监听消息事件
socket.addEventListener('message', function(event) {
  console.log('Received message:', event.data);
  // 关闭连接
  socket.close();
});

// 监听关闭事件
socket.addEventListener('close', function() {
  console.log('Disconnected from server');
});

通过以上代码,客户端和服务器之间建立了 WebSocket 连接,并可以相互发送消息。注意,WebSocket 的连接 URL 使用了 ws:// 协议,而不是传统的 HTTP 或 HTTPS 协议

4、window.parent.postMessage

它是一种用于在不同域之间进行跨文档通信的机制。它可以在包含 iframe 的父窗口和 iframe 内的子窗口之间传递数据,即使它们位于不同的域。

4.1 demo

在父窗口中的代码:

// 监听子窗口发送的消息
window.addEventListener('message', function(event) {
  const origin = event.origin || event.originalEvent.origin;
  if (origin === 'http://example.com') {
    console.log('Received message from child:', event.data);
  }
});

// 向子窗口发送消息
var iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage('Hello from parent!', 'http://example.com');

在子窗口中的代码:

// 监听父窗口发送的消息
window.addEventListener('message', function(event) {
  const origin = event.origin || event.originalEvent.origin;
  if (origin === 'http://parent.com') {
    console.log('Received message from parent:', event.data);
    // 向父窗口发送消息
    window.parent.postMessage('Hello from child!', 'http://parent.com');
  }
});

在上面的示例中,父窗口和子窗口分别位于不同的域名下(父窗口为 http://parent.com,子窗口为 http://example.com)。在父窗口中,通过监听 message 事件来接收子窗口发送的消息,然后使用 event.origin 来验证消息来源的域名,确保只接收来自特定域名的消息。然后,通过 window.postMessage 向子窗口发送消息。

在子窗口中,也通过监听 message 事件来接收父窗口发送的消息,同样使用 event.origin 进行验证。然后,通过 window.parent.postMessage 向父窗口发送消息。这样,父窗口和子窗口就可以通过 postMessage 方法进行跨域通信,实现数据的传递和交互。

4.2 适用场景

通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另外一个页面判断来源并接收消息。

  • 嵌套页面通信:当一个页面中包含嵌套的 iframe,而需要父窗口和子窗口之间进行通信时,可以使用 postMessage 来传递消息和数据。例如,在父窗口中操作子窗口的内容或接收子窗口发送的消息。
  • 跨域通信:当父窗口和子窗口位于不同的域名下,但需要进行跨域通信。例如,在一个页面中嵌入了来自不同域的广告或外部组件,需要与这些组件进行通信。
  • 页面间的协作:在多个窗口或标签页之间共享数据或进行页面间的协作时,可以使用 postMessage 来发送消息和触发特定的操作。例如,一个窗口中的表单提交后,可以通过 postMessage 通知其他窗口进行更新或展示相关信息神奇
  • 前端组件间的通信:在复杂的前端应用中,可能存在多个独立的组件(组件指的是一种可重用的、独立的模块,用于构建用户界面的一部分),需要进行数据传递和通信。通过 postMessage 可以实现不同组件之间的解耦和通信。例如,一个组件产生的事件需要通知其他组件进行相应的更新。

总的来说,window.parent.postMessage 可以在嵌套页面、跨域通信、页面协作以及前端组件间的通信等业务场景下使用,提供了一种安全、可靠的方式来在浏览器环境下实现跨窗口的数据传递和交互。

5、document.domain

它是用于解决特定的跨域通信问题的一种方法。适用于在两个窗口(或框架)之间具有相同顶级域名但不同子域的情况。 要使用 document.domain 进行跨域通信,需要满足以下条件:

  1. 两个窗口的顶级域名必须相同,例如一个窗口的域名是 example.com,另一个窗口的域名是 subdomain.example.com
  2. 两个窗口的协议(http 或 https)必须相同。
  3. 在两个窗口中,通过设置 document.domain 属性为相同的顶级域名。

5.1 demo

在父窗口(example.com)中的代码:

document.domain = 'example.com';

function receiveMessage(event) {
  const origin = event.origin || event.originalEvent.origin;
  if (origin === 'http://subdomain.example.com') {
    console.log('Received message:', event.data);
  }
}
window.addEventListener('message', receiveMessage, false);

在子窗口(subdomain.example.com)中的代码:

document.domain = 'example.com';

// 向父窗口发送消息
window.parent.postMessage('Hello from subdomain', 'http://example.com');

在这个例子中,通过设置 document.domain 为相同的顶级域名,父窗口和子窗口之间就可以进行跨域通信。父窗口通过监听 message 事件接收来自子窗口的消息,并根据来源进行处理。子窗口使用 postMessage 方法向父窗口发送消息。

需要注意的是,document.domain 只适用于特定的跨域通信场景,且仅在满足条件的情况下才能使用。对于其他跨域通信需求,可能需要使用更强大的跨域解决方案,如 CORS 或 JSONP。

6、使用代理服务器

通过在自己的服务器上设置一个代理,将跨域请求转发到目标服务器,然后再将响应返回给浏览器。这种方法需要在自己的服务器上进行配置,并且对于大量的请求会增加服务器的负担。

举例说明:假设你有一个网站A,网站A需要向网站B发起一个跨域请求。你可以在网站A的服务器上设置一个代理,当用户访问网站A时,代理会将请求转发到网站B,然后再将响应返回给用户。这样,用户感觉就像是在访问网站A,但实际上请求是通过代理转发到网站B的。

优点:可以解决跨域问题,不需要修改目标服务器的配置,更适合于小型项目。

缺点:需要在自己的服务器上进行配置,增加了服务器的负担。对于大量的请求,可能会影响服务器性能。

7、使用 Nginx 反向代理

通过配置 Nginx 服务器作为反向代理,将跨域请求转发到目标服务器,同时在 Nginx 配置中设置相应的跨域头部信息。

举例说明:假设你有一个网站A,网站A需要向网站B发起一个跨域请求。你可以在网站A的服务器上配置 Nginx 作为反向代理,将请求转发到网站B。同时,在 Nginx 配置中设置跨域头部信息,这样浏览器就允许网站A发起跨域请求。

优点:可以解决跨域问题,同时可以在 Nginx 配置中灵活地设置跨域头部信息。

缺点:需要在自己的服务器上配置 Nginx,并且需要一定的 Nginx 配置知识。对于大量的请求,可能会影响服务器性能。

以上方法的适用性和实施方式会根据具体情况而有所不同。选择哪种方法取决于你的需求和环境。此外,对于一些敏感的操作(如修改数据、删除数据等),需要额外的安全措施来确保只有授权的请求可以进行这些操作,以防止安全风险。