切换语言为:繁体

前端的跨域问题以及常见的解决跨域问题的方法

  • 爱糖宝
  • 2024-09-16
  • 2045
  • 0
  • 0

在前端的开发中,跨域问题非常常见,尤其是在涉及到前后端分离的应用中更为常见。我在最近的面试当中也会被常常问到,所以我学习了一下,来写篇文章来加深印象。

首先,我们需要知道为什么会存在跨域问题

跨域问题的存在主要是出于安全考虑。浏览器实施同源策略(Same-origin policy)是为了防止恶意网站在用户不知情的情况下,利用用户的认证信息(如 Cookie)或者其他敏感数据进行恶意操作。以下是几个关键原因,说明为什么需要有跨域限制:

安全性

防止数据泄露:同源策略可以防止恶意站点通过脚本获取用户在另一个网站上的敏感信息。例如,一个恶意站点可以尝试通过脚本读取银行网站上的用户账户信息,如果没有跨域限制,这种攻击将更容易成功。

防止 CSRF 攻击:跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种攻击方式,攻击者诱导受害者在一个已经登录的 Web 应用程序上执行非本意的操作。同源策略有助于防止这种情况的发生,因为攻击者的网站无法直接与目标网站进行交互。

保护隐私:跨域限制可以防止第三方网站追踪用户的浏览习惯。如果没有跨域限制,第三方网站可以通过脚本访问其他网站上的信息,从而收集用户的个人信息。

维护网站边界

明确数据边界:同源策略明确了各个网站的数据边界,使得每个网站负责自己的数据和服务,减少了数据混乱的风险。

减少意外行为:同源策略可以帮助防止一个网站的脚本意外地影响到另一个网站的功能,保持各自网站的功能独立性和完整性。

实现机制

同源策略的实现机制是通过浏览器来强制执行的。当浏览器检测到请求的源(协议、域名、端口号)与当前页面的源不同时,就会阻止请求的响应数据被脚本访问。

当我们了解完跨域问题的本质后,来说说如何来解决跨域

解决跨域的方式有挺多种的,我今天要说的有“JSONP,CORS,代理服务器,postMessage,web Sorket” 接下来我一个一个的讲解

首先是JSONP

JSONP

我们在写html文件的时候,经常会使用到script标签的src属性去请求外部的CDN,诶,既然是请求,那不就应该会同源策略的影响吗。经过一系列的尝试,发现它不会受同源策略的影响,至于原因嘛,我去问了下ai,它总结的比较完善。

为什么 <script> 标签不受同源策略影响

历史原因:在同源策略提出之前,<script> 标签就已经被广泛使用了,而且它的设计初衷是为了让开发者能够方便地引入外部脚本。因此,浏览器对 <script> 标签的支持一开始就包含了从任何源加载的能力。

安全性和实用性权衡:虽然 <script> 标签允许从任何源加载脚本,但浏览器依然会对加载的脚本进行某些安全检查。例如,浏览器会阻止恶意脚本的执行,尤其是那些试图进行跨站脚本攻击(XSS)的脚本。此外,现代浏览器还会对加载的脚本进行 CSP(Content Security Policy)检查,进一步增强了安全性。

功能需求:在现代 Web 开发中,很多 JavaScript 框架和库都是通过 <script> 标签从 CDN 或其他远程服务器加载的。如果 <script> 标签受到同源策略的严格限制,将会极大地影响这些框架和库的使用。

既然script标签的src不受同源策略的影响,那么我们不就可以使用它来给发送请求了吗。

所以我们可以这样写

<script src="http://localhost:3000"></script>

这时候就有小伙伴要问了,可是我们请求接口不是为了拿到数据吗,这样怎么拿到数据呢。别急接下来我就会告诉你

向后端请求时我们通常会携带参数,由于我们利用src来发送请求所以这里我们使用get请求在请求的url上携带参数,我们可以携带一个函数,并且要在前端window环境里去挂载这个函数,在后端写成调用的形式传给前端,参数就是要传的数据。这样的话函数会直接在window环境下直接调用,那么就可以拿到数据了 具体实现如下:

前端

<script>
      function jsonp(url, cb) {
        return new Promise(function(resolve, reject) {
          const script = document.createElement('script');
          window[cb] = function(data) {   // callback()
            resolve(data)
          }
  
          script.src = `${url}?cb=${cb}`;
          document.body.appendChild(script);
        });
      }
  
  
      jsonp('http://localhost:3000', 'callback').then(res => {
        console.log(res);
      })
    </script>

后端

const http = require('http');

http.createServer(function (req, res) {
  // res.end('hello world')
  const query = new URL(req.url,`http://${req.headers.host}`).searchParams
  const cb = query.get('cb')
  if(cb) {
    const data = 'hello world'
    const result = `${cb}("${data}")`
    res.end(result)
  }

}).listen(3000);

这就是JSONP的具体实现过程了

但是JSONP有许多缺点,它只能发送get请求,这就让数据的传输不那么安全,而且它需要前后端的配合,使用起来比较麻烦,接下来的cors就比较完美了

cors

在前后端项目中,我常常使用cors中间件来解决跨域问题,那么它的原理是什么呢。

其实它的原理特别简单,就是在响应头中添加几个字段:

const http = require('http');

http.createServer(function (req, res) {
  // 开启cors
  res.writeHead(200, {
    // 设置允许的源
    'access-control-allow-origin': "*"
  })

  res.end('hello world')
}).listen(3000);

关于这些字段:

  1. Access-Control-Allow-Origin

    • 注意:当设置为 "*" 时,不能包含 Access-Control-Allow-Credentials,因为后者要求源必须是具体的,而不是通配符。

    • 用途:指定哪些源可以访问资源。

    • 示例: "*":表示允许任何源访问。 "example.com":表示只允许来自 example.com 的请求访问。 ["example.com", "anotherdomain.com"]:允许多个特定源访问。

  2. Access-Control-Allow-Credentials

    • 注意:如果设置为 true,则 Access-Control-Allow-Origin 必须是一个具体的源,不能是 "*"。

    • 用途:指示响应是否应该允许包含凭证(如 cookies、HTTP 认证信息等)。

    • 示例: true:允许包含凭证。 false:不允许包含凭证。

  3. Access-Control-Allow-Methods

    • 用途:指定允许的 HTTP 方法。

    • 示例: "GET, POST, OPTIONS":允许 GET、POST 和 OPTIONS 方法。

  4. Access-Control-Allow-Headers

    • 用途:指定请求中允许的头部字段。

    • 示例: "Authorization, X-Requested-With, Content-Type, Accept":允许这些头部字段。

  5. Access-Control-Max-Age

    • 用途:指定 Preflight 请求的结果可以被缓存的秒数。

    • 示例: 86400:表示结果可以被缓存一天(86400 秒)。

  6. Access-Control-Expose-Headers

    • 用途:指定哪些响应头部字段可以被客户端访问。

    • 示例: "Custom-Header, Another-Header":允许客户端访问这些响应头部字段。

任何就是postMessgae了

postMessage

这个通常时父级页面使用postMessage向使用iframe内嵌在自己内部的子级页面进行相互通信

postMessage 的基本原理

postMessage 是一种允许两个不同源的窗口(如两个不同域名下的窗口)互相发送消息的 API。它提供了跨域通信的能力,同时也可用于同一域名下的不同窗口或 iframe 之间的通信。

使用 postMessage 的步骤

  1. 父页面发送消息

父页面可以使用 contentWindow.postMessage() 方法向子页面发送消息。这里 contentWindow 是子页面的 window 对象,可以通过 iframe 的 contentWindow 属性获得。

<!-- 父页面 index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Parent Page</title>
</head>
<body>
    <iframe id="childFrame" src="child.html"></iframe>
    <button onclick="sendMessage()">Send Message</button>

    <script>
        function sendMessage() {
            var childWindow = document.getElementById('childFrame').contentWindow;
            childWindow.postMessage({ type: 'hello', message: 'Hello from parent!' }, '*');
        }
    </script>
</body>
</html>

  1. 子页面接收消息

子页面需要监听 message 事件,以便接收到来自父页面的消息。

<!-- 子页面 child.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Child Page</title>
</head>
<body>
    <script>
        window.addEventListener('message', function(event) {
            if (event.origin !== 'http://parentdomain.com') {
                return; // 只接受来自父域的消息
            }
            console.log('Received message:', event.data);
        });
    </script>
</body>
</html>

重要注意事项

  1. 来源验证:接收方应当验证 event.origin,确保消息来自预期的源。这可以防止恶意页面冒充父页面发送消息。

  2. 目标源:发送消息时,可以指定 targetOrigin 参数来限制消息只能发送到特定的源。如果设置为 "*", 则表示可以发送到任何源。

  3. 数据格式postMessage 发送的数据可以是任何可以序列化的 JavaScript 对象,包括字符串、数组、对象等。

  4. 跨域限制postMessage 虽然可以跨域通信,但仍然受限于同源策略。接收方需要验证消息的来源。

示例:双向通信

除了父页面向子页面发送消息外,子页面也可以向父页面发送消息,并且父页面也需要监听 message 事件。

<!-- 父页面 index.html -->
<script>
    function sendMessage() {
        var childWindow = document.getElementById('childFrame').contentWindow;
        childWindow.postMessage({ type: 'hello', message: 'Hello from parent!' }, '*');
    }

    window.addEventListener('message', function(event) {
        if (event.origin !== 'http://childdomain.com') {
            return; // 只接受来自子域的消息
        }
        console.log('Received message:', event.data);
    });
</script>

<!-- 子页面 child.html -->
<script>
    window.addEventListener('load', function() {
        var parentWindow = window.parent;
        parentWindow.postMessage({ type: 'reply', message: 'Reply from child!' }, '*');
    });

    window.addEventListener('message', function(event) {
        if (event.origin !== 'http://parentdomain.com') {
            return; // 只接受来自父域的消息
        }
        console.log('Received message:', event.data);
    });
</script>

然后是webSocket

webSocket

webSocket 它是socket协议本身就不受影响 使用示例:

前端

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
     function web_Socket(url, params) {
      return new Promise((resolve) => {
        const socket = new WebSocket(url)
        socket.onopen = () => {
          socket.send(JSON.stringify(params))
        }
        socket.onmessage = (res) => {
          resolve(res.data)
        }
      })
    }

    web_Socket('ws://localhost:3000', {}).then(data => {
      console.log(data);
    })
  </script>
</body>
</html>

后端

const WebSocket = require('ws')
const ws = new WebSocket.Server({ port: 3000 })
ws.on('connection', (obj) => {
  obj.on('message', (data) => {
    obj.send('this is a message from WebSocket')
  })
})

最后就是代理服务器了

代理服务器

简单来讲就是客户端发请求给代理服务器,代理服务器发请求给目标服务器,代理服务器接收到目标服务器的响应后,再将响应返回给客户端。这个过程使得客户端与目标服务器之间的直接通信变成了客户端与代理服务器之间的通信,从而绕过了浏览器的同源策略限制。

代理服务器解决跨域的步骤

  1. 客户端配置代理服务器:客户端(通常是前端应用)配置代理服务器,将原本需要发送到目标服务器的请求,改为发送到本地运行的代理服务器。

  2. 代理服务器接收请求:代理服务器接收到客户端发送的请求后,解析请求信息。

  3. 代理服务器转发请求:代理服务器将请求转发到目标服务器。在此过程中,代理服务器可以修改请求头,例如添加或修改 Origin 字段,以适应目标服务器的要求。

  4. 目标服务器响应:目标服务器处理请求,并返回响应数据。

  5. 代理服务器接收响应:代理服务器接收到目标服务器的响应后,可能需要对响应头进行处理,例如去除某些响应头字段或添加新的响应头字段。

  6. 代理服务器返回响应:代理服务器将处理后的响应返回给客户端。

有关跨域的分享就到这里了

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.