拓展边界:前端世界的跨域挑战

目录

什么是跨域

概念

同源策略及限制内容

常见跨域场景

如何解决跨域

CORS

Nginx代理跨域

Node中间件代理跨域

WebSocket

postMessage

JSONP

其他


什么是跨域

概念

在此之前,我们了解一下一个域名地址的组成:

跨域指的是在网络安全中,由于浏览器的同源策略(Same-Origin Policy)限制,当一个网页的协议、域名或端口与另一个网页的协议、域名或端口不同时,就存在跨域问题。如果缺少同源策略的限制,可能会导致安全隐患,如跨站脚本攻击(XSS)或跨站请求伪造(CSRF)等。

同源策略及限制内容

同源策略是浏览器的一项安全机制,它限制了来自不同源的脚本对当前文档的访问。同源策略要求资源必须来自同一个源(即协议、域名和端口相同),并限制了对以下内容的访问:

  1. Cookie、LocalStorage、IndexedDB 等存储性内容:不允许在不同源之间读取或写入这些数据。

  2. DOM 节点:不允许获取来自不同源的 DOM 元素。

  3. AJAX 请求发送后的响应数据:如果发起了跨域的 AJAX 请求,浏览器会阻止读取其响应内容。

但是有三个标签允许跨域加载资源,它们不受同源策略限制:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

常见跨域场景

跨域存在于协议、子域名、主域名、端口号中任何一个不相同时。即使两个不同的域名指向同一个 IP 地址,只要其中任何一部分不同,就被视为不同源。

两个重要点:

  1. 协议和端口造成的跨域问题需要后端处理,前端无法解决。
  2. 浏览器在判断跨域时主要依据 URL 的首部,即协议、域名和端口必须匹配。

跨域并不是指请求发不出去,而是浏览器限制了读取来自其他域名下内容的安全策略。例如,Ajax 请求会被浏览器拦截响应数据,但表单提交不会获取新内容,所以可以发起跨域请求。同样,跨域并不能完全阻止 CSRF,因为请求依然会发送出去,只是浏览器会拦截响应。

如何解决跨域

CORS

CORS 通信过程都是浏览器自动完成,需要浏览器(都支持)和服务器都支持,所以关键在只要服务器支持,就可以跨域通信,CORS请求分两类:简单请求和非简单请求。

CORS请求默认不包含Cookie以及HTTP认证信息,如果需要包含Cookie,涉及跨域请求携带 Cookie 信息时需要一些特殊设置,其中涉及到 CORS 的头部设置以及前端代码的配置。

假设有两个域名:https://example.com 和 https://api.example.com,前者是网页的域名,后者是提供 API 的域名。下面是一个示例,演示如何设置跨域请求以在包含 Cookie 的情况下进行:

后端设置 CORS 头部

在 API 的响应中设置 CORS 相关头部:

// Express 示例
const express = require('express');
const app = express();
 
// 设置允许跨域请求的域名和允许携带 Cookie
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://example.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  next();
});
 
// 处理其他路由或请求
// ...
 
app.listen(3000);

前端发送带有 withCredentials 的请求

在前端,如果想要在跨域请求中携带 Cookie,需要在 XMLHttpRequest 或 Fetch 请求中设置 withCredentials 为 true

使用 XMLHttpRequest 的示例:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.withCredentials = true; // 允许携带 Cookie
xhr.send();

或者使用 Fetch API:

fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include', // 允许携带 Cookie
});

上述代码中的关键点在于将 withCredentials 或 credentials: 'include' 设置为 true,这样浏览器就会在请求中包含当前页面的 Cookie 信息。但要确保后端 API 设置了正确的 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials 头部,并且不要使用 * 通配符,而是指定了明确的允许跨域的域名。

简单请求

简单请求指的是符合一定条件的跨域请求类型,在这种请求下,浏览器会自动在头部中添加 Origin 字段,表示请求的来源域,服务器根据这个来源决定是否允许请求。满足以下两个条件之一的请求被认为是简单请求:

1.请求方法是 HEAD、GET、POST 之一

2.请求头信息不超过一定限制。允许的请求头包括:

  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-Id

  • Content-Type:允许的值为 application/x-www/form/urlencoded、multipart/form-data、text/plain 之一。

这些条件主要为了兼容早期的表单提交方式,因为历史上表单提交一直可以跨域。

在这样的简单跨域请求中,服务器至少需要设置 Access-Control-Allow-Origin 头部来允许特定来源的请求。如果服务器认可请求,会在响应中返回以下必要的 CORS 头部:

Access-Control-Allow-Origin 这个字段是必须的,它指定了允许访问的来源域,一般是请求中 Origin 字段的值,但也可以是通配符 *,表示允许任意来源。
Access-Control-Allow-Credentials 这个字段是可选的,表示是否允许发送 Cookie。如果需要跨域请求携带身份凭证(比如 Cookie 或 HTTP 认证信息),服务器需要设置这个字段为 true。
Access-Control-Expose-Headers 这个字段是可选的,它指定了响应中可以被前端访问的其他头部信息。
Content-Type 表示响应的文档类型和字符编码。

简单请求的处理相对较为简单,但需要服务器允许特定的跨域访问并设置相应的 CORS 头部来确保安全。

非简单请求

非简单 CORS 请求指的是不符合简单请求条件的跨域请求,如使用了 PUT 或 DELETE 方法,或者请求的 Content-Type 是 application/json 等。对于这样的请求,浏览器会在正式发起请求之前先发送一个 OPTIONS 类型的预检请求。

预检请求的目的是向服务器查询是否允许来自网页所在域名的请求,并了解可以使用哪些额外的头信息字段。这个预检请求(OPTIONS 请求)的头部信息包括:

  • Origin: 标识请求来自哪个域,是必须的字段。
  • Access-Control-Request-Method: 列出 CORS 请求会用到的 HTTP 方法,也是必须的字段。
  • Access-Control-Request-Headers: 指定 CORS 请求会额外发送的头信息字段,用逗号隔开。

服务器收到这个预检请求后,根据收到的信息判断是否允许这个请求,如果允许,则返回相应的 CORS 头部信息。除了允许特定的跨域请求,服务器还可以在响应头部中添加 Access-Control-Max-Age 字段来缓存预检请求的结果,减少后续请求的预检次数。这个缓存只对完全一样的 URL 生效,超出缓存时间后,再次发起相同的跨域请求仍会触发预检请求。

Nginx代理跨域

Nginx可以用作反向代理服务器,来解决跨域问题。它可以代理客户端的请求,并将请求发送到目标服务器,再将目标服务器的响应返回给客户端。这种方法类似于 CORS 跨域原理,但是它是通过服务器端进行设置,而不是在浏览器端进行设置。

以下是一个简单的 Nginx 配置示例,用于处理跨域请求:

server {
    listen 81;
    server_name www.domain1.com;
 
    location / {
        # 设置反向代理
        proxy_pass http://xxxx1:8080;
 
        # 修改 Cookie 中的域名
        proxy_cookie_domain www.xxxx1.com www.xxxx2.com;
 
        # 添加头部信息以处理跨域请求
        add_header Access-Control-Allow-Origin http://www.xxxx2.com; # 当前端只跨域不带 Cookie 时,可以使用 * 
        add_header Access-Control-Allow-Credentials true;
    }
}

这段配置的作用如下:

proxy_pass 指令用于指定反向代理的目标服务器地址。proxy_cookie_domain 用于修改代理服务器返回的响应中的 Cookie 中的域名,将它从源域名修改为目标域名,确保 Cookie 在跨域时仍然有效。add_header 指令用于添加响应头部信息,其中 Access-Control-Allow-Origin 允许特定来源的跨域请求,Access-Control-Allow-Credentials 表示是否允许请求携带身份凭证(例如 Cookie)。

这种方法通过 Nginx 作为代理服务器,处理了跨域请求的响应头部,允许指定的域名进行跨域请求。这样就可以在不修改前端代码的情况下,实现跨域请求的需求。

Node中间件代理跨域

在 vue.config.js 文件中,可以通过配置 devServer 的 proxy 选项实现代理转发:

module.exports = {
    // 其他配置...
    devServer: {
        proxy: {
            [process.env.VUE_APP_BASE_API]: {
                target: 'http://xxxx', // 代理跨域目标接口
                ws: true,
                changeOrigin: true,
                pathRewrite: {
                    ['^' + process.env.VUE_APP_BASE_API]: ''
                }
            }
        }
    }
}
  • proxy 字段用于配置代理。process.env.VUE_APP_BASE_API 是 Vue 项目中定义的环境变量,表示需要被代理的请求路径前缀。

  • target 指定了代理的目标地址,即请求将会被代理到这个地址下。

  • ws 表示是否代理 WebSocket。

  • changeOrigin 设置为 true 后,可以突破浏览器的同源策略,允许跨域。

  • pathRewrite 允许对请求路径进行重写,将匹配到的路径前缀去掉。比如,如果请求路径是 /api/user,经过 pathRewrite 后就会变成 /user,将前缀 /api 去掉了。

当使用 Node 中间件进行代理跨域时,你可以使用 http-proxy-middleware 包来创建一个代理服务器,让后端服务(如 Express)处理跨域请求。这个中间件可以在 Express 应用程序中使用,允许你在服务器端修改请求和响应,以便处理跨域问题。

下面是一个在 Express 中使用 http-proxy-middleware 中间件的简单示例:

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
 
const app = express();
 
app.use('/', createProxyMiddleware({ 
    // 代理跨域目标接口 
    target: 'http://xxxx:8080', 
    changeOrigin: true, 
    // 修改响应头信息,实现跨域并允许带cookie 
    onProxyRes: function(proxyRes, req, res) { 
        res.header('Access-Control-Allow-Origin', 'http://xxxx');
        res.header('Access-Control-Allow-Credentials', 'true');
    }, 
    // 修改响应信息中的 cookie 域名 
    cookieDomainRewrite: 'www.domain1.com' // 可以为 false,表示不修改
})); 
 
app.listen(3000);

这段代码做了以下事情:

  • 引入了 Express 框架和 http-proxy-middleware。

  • 创建了一个 Express 应用。

  • 使用 app.use 将 createProxyMiddleware 中间件应用到根路径 /。

  • 配置了 createProxyMiddleware,指定了目标服务器的地址 target,开启了 changeOrigin 选项以确保源头地址被更改为目标地址。

  • 在 onProxyRes 中修改了响应头部信息,允许跨域并设置了允许携带凭证(如 Cookie)。

  • cookieDomainRewrite 可以用于修改响应信息中的 Cookie 域名,使其在传输中适应跨域情况。

这种方式可以在后端服务器层面对跨域进行处理,允许前端应用向不同域名的后端服务发出请求。

WebSocket

WebSocket是一种在单个 TCP 连接上进行全双工通信的协议,它允许客户端和服务器之间进行实时数据交换。与传统的 HTTP 请求不同,WebSocket 的连接是持久性的,双方可以随时发送数据而不需要等待请求。

在跨域方面,WebSocket协议与传统的 HTTP 请求不同,它不受同源策略的限制。可以使用 ws://(非加密)和 wss://(加密)作为协议前缀,不受同源策略的约束,只要服务器支持 WebSocket,即可与客户端进行通信。

WebSocket请求头中包含 Origin 字段,用于指示请求的来源域。服务器可以检查这个字段来判断是否允许与特定域的客户端建立 WebSocket 连接。如果客户端的来源域在服务器的白名单内,服务器将允许该连接的建立,否则可能会拒绝连接或执行其他安全措施。

通过 WebSocket,服务器和客户端之间可以进行持久的双向通信,这种通信方式适用于实时性要求较高的场景,如在线聊天、实时数据传输等。由于不受同源策略的限制,WebSocket提供了一种跨域通信的可选方案。

下面是一个简单的例子,展示了如何使用 WebSocket 进行跨域通信。

首先,假设有两个域名分别是 domain1.com 和 domain2.com,我们将在 domain1.com 的页面上创建 WebSocket 连接到 domain2.com 的服务器:

在 domain2.com 的服务器端,你需要有一个 WebSocket 服务器,例如基于 Node.js 的 ws 模块,用于监听 WebSocket 连接:

const WebSocket = require('ws');
 
const wss = new WebSocket.Server({ port: 8080 });
 
wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('Received: %s', message);
    // 可以回复客户端消息
    ws.send('Hello, client!');
  });
});

在 domain1.com 的页面上,你可以使用 JavaScript 来创建 WebSocket 连接到 domain2.com 的服务器:

const socket = new WebSocket('ws://domain2.com:8080'); // WebSocket服务器的地址
 
socket.onopen = function(event) {
    console.log('WebSocket连接已打开');
    socket.send('Hello, server!');
};
 
socket.onmessage = function(event) {
    console.log('收到服务器消息:', event.data);
};
 
socket.onclose = function(event) {
    if (event.wasClean) {
        console.log('连接已关闭');
    } else {
        console.log('连接断开');
    }
};
 
socket.onerror = function(error) {
    console.error('WebSocket连接错误:', error);
};

在这个例子中,domain1.com 的页面通过 WebSocket 连接到了 domain2.com 的服务器,并向其发送了消息。服务器收到消息后可以进行处理,并且也可以通过 WebSocket 向客户端发送消息。

需要注意的是,WebSocket 协议在设计上允许跨域通信,因此不会受到浏览器同源策略的限制。但是,服务器端仍然可以根据自己的逻辑来判断是否接受来自特定来源的连接。

postMessage

postMessage 是 HTML5 中的一项 API,它允许在不同窗口(包括 iframe)或不同来源的文档间进行安全地消息传递。这种通信机制可以用于多种场景:

1️⃣ 页面和新打开的窗口之间的数据传递

2️⃣ 不同窗口(包括不同域的窗口)之间的通信

3️⃣ 页面与嵌套的 iframe 之间的数据传递

4️⃣ 不同源的跨域传递数据

postMessage 接受两个参数:

第一个参数是要发送的数据,可以是字符串、对象等。

第二个参数是指定消息发送的目标窗口的来源。这个参数通常是一个目标窗口的 origin(协议 + 域名 + 端口),也可以是通配符 *(表示任意窗口),或者 /(表示与当前窗口同源的窗口)。

例如,在一个页面中,可以使用以下方式向另一个窗口发送消息:

// 获取目标窗口对象
const targetWindow = window.open('https://example.com', '_blank');
 
// 向目标窗口发送消息
targetWindow.postMessage({ message: 'Hello, window!', value: 42 }, 'https://example.com');

在目标窗口中,可以监听 message 事件来接收来自其他窗口的消息:

// 监听 message 事件
window.addEventListener('message', function(event) {
    // 判断消息来源是否符合预期
    if (event.origin === 'https://sending-origin.com') {
        // 处理接收到的消息
        console.log('Received message:', event.data);
    }
});

JSONP

JSONP(JSON with Padding)是一种利用 <script> 标签实现的跨域数据请求方法。其原理是通过动态创建 <script> 标签,向服务器请求数据,并在请求的 URL 中包含一个回调函数名。服务器收到请求后,将数据包裹在回调函数中返回给客户端,从而绕过浏览器的同源策略限制。

JSONP 主要用于 GET 请求,并不适用于其他 HTTP 方法,且存在安全隐患,容易受到 XSS(跨站脚本攻击)的影响。但它有一些优点,比如可以向不支持 CORS 的旧浏览器或不支持跨域请求的网站请求数据。

使用 JSONP 的示例代码如下:

// 创建一个包含回调函数的 URL
const url = 'http://juejin.com/xxx?callback=handleCallback';
 
// 创建一个 script 标签
const script = document.createElement('script');
 
// 设置 script 的 URL
script.src = url;
 
// 将 script 添加到页面中
document.body.appendChild(script);
 
// 定义回调函数,处理从服务器返回的数据
function handleCallback(res) {
    console.log(res);
}

服务器接收到这个请求后,会将数据放在 handleCallback 函数中返回,例如:

handleCallback({ code: 200, msg: 'success', data: [] });

在客户端,这个函数 handleCallback 将立即执行,从而完成数据的处理和操作。

虽然 JSONP 具有一定的局限性和安全风险,但在某些场景下仍然是一种有效的跨域数据获取方式。

其他

除了以上提到的一些常见的解决跨域的方法以外,还有其他的方式可以来解决这个问题。

1. document.domain + iframe:适用于主域名相同但子域名不同的跨域场景。当两个页面的 document.domain 设置为同一个值(主域名),它们就可以相互通信。但这种方法仅适用于主域名相同的情况。

2. window.name + iframe:利用 iframe 的 window.name 属性,这个属性在不同页面或者不同域名加载后依然保持不变。通过在不同页面的 iframe 中设置和读取 window.name,可以实现跨域通信。window.name 具有持久性,且可以存储较大量的数据。

3. location.hash + iframe:适用于页面间利用 URL 的 hash 部分来传递信息,特别是在 C 页面帮助 A 页面和 B 页面进行通信。通过改变 URL 的 hash 部分来传递信息,在不同页面之间进行简单的数据交换。

这些方法虽然可以解决一些特定的跨域通信问题,但由于它们的特性和限制,通常只适用于特定场景,并且可能存在一些安全性或者容量方面的限制。因此,在使用这些方法时,需要仔细考虑其适用范围和可能带来的安全隐患。

好啦,本文就到这里结束了,感谢阅读~~

猜你喜欢

转载自blog.csdn.net/weixin_51735748/article/details/135714461