跨域相关的问题

1 什么是跨域?

跨域本质是浏览器基于同源策略的一种安全手段,同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能,所谓同源(即指在同一个域)具有以下三个相同点:

  • 协议相同(protocol)
  • 主机相同(host)
  • 端口相同(port)

反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制

2 什么是同源策略?为什么浏览器会有同源策略?

同源策略(Same-Origin Policy)是Web浏览器的一项安全特性,旨在限制一个源(origin)的文档或脚本如何与另一个源的资源进行交互。

同源策略限制:

  • cookie localStorage // 如果支持别人就可以拿到你的 cookie
  • iframe // 如果支持比如你 iframe 嵌入百度,就可以拿到百度的信息
  • ajax // 如果跨域你就访问别人的请求

为什么浏览器需要同源策略?

  1. 防止信息泄露:没有同源策略,恶意网站可以通过脚本轻易读取银行网站或其他敏感站点的用户数据,如登录凭证、个人资料等。
  2. 保护用户隐私:同源策略限制了第三方网站对用户本地存储信息(如Cookie)的访问,保护了用户的隐私。
  3. 防御攻击:它有助于减少跨站脚本(XSS)和跨站请求伪造(CSRF)等安全漏洞,这两种攻击都是通过利用不同源之间缺乏隔离来实施的。

3 解决跨域的方法

3.1 jsonp

缺点:只支持 get请求, 如果提供jsp 的服务端不安全,他可以操作你的整个js 很危险

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <script>
      function show(data) {
        console.log(data); // 跨域的数据
      }
    </script>
    <script src="http://localhost:3000/test?wd=4141&cb=show"></script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>\
  <body>
    <script>
      function jsonp({ url, params, cb }) {
        return new Promise((resolve, reject) => {
          const script = document.createElement("script");
          window[cb] = function(data) {
            resolve(data);
            document.body.removeChild(script);
          };
          params = { ...params, cb };
          const arrs = Object.keys(params).map(key => `${key}=${params[key]}`);
          script.src = `${url}?${arrs.join("&")}`;
          document.body.appendChild(script);
        });
      }
      jsonp({
        url: "http://localhost:3000/test",
        params: { wd: "414" },
        cb: "show"
      }).then(data => {
        console.log(data);
      });
    </script>
  </body>
</html>

服务端:

const express = require("express");
const app = express();
app.get("/test", (req, res) => {
  let { wd, cb } = req.query;
  res.end(`${cb}('跨域的数据'); console.log(11)`);
});
// content-type是 application/javascript
app.listen(3000);

3.2 CORS

CORS(Cross-Origin Resource Sharing,跨源资源共享)是一种网络安全策略,它允许浏览器有选择地放宽同源策略(Same Origin Policy),从而使Web页面可以从不同的源(协议、域名、端口)请求资源,而不会受到浏览器的跨域限制。

缺点:会有一个预检请求

  • 对于复杂请求(如使⽤⾮简单⽅法:PUT, DELETE 或⾃定义头),浏览器会先发送⼀个OPTIONS请求,询问服务器是否允许跨域请求
  • 服务器如果同意跨域请求,则返回包含CORS头信息的响应
let express = require("express");
let app = express();
let whitList = ["http://localhost:3000"];
app.use((req, res, next)=> {
  let origin = req.headers.origin; // 取那个源访问的我,这里是 localhost:3000
  if (whitList.includes(origin)) {
    // 设置哪个源可以访问我
    res.setHeader("Access-Control-Allow-Origin", origin);
    // 允许携带哪个头访问我
    res.header("Access-Control-Allow-Headers", "Content-Type");
    // 允许哪个方法访问我
    res.setHeader("Access-Control-Allow-Methods", "GET,POST");
    // 允许携带cookie
    res.setHeader("Access-Control-Allow-Credentials", true);
    //预检测 多久检测一次(多久发送一次 options请求)
    res.setHeader("Access-Control-Max-Age", 6);
    // 允许前端获取哪个头
    res.setHeader("Access-Control-Expose-Headers", "name");  // xhr.setRequestHeader('name','wxj')
    res.header('Content-Type', 'application/json;charset=utf-8');
  }
  next()
});
app.get("/getData", function(req, res) {
  res.end("i love you");
});
app.listen(4000);

3.3 postMessage

postMessage 方法允许一个窗口向另一个窗口发送消息,无论这两个窗口是否同源

<!DOCTYPE html>
<html>
<head>
    <title>发送消息页面</title>
</head>
<body>
    <iframe id="myIframe" src="http://receiver.example.com/receiver.html" width="600" height="400"></iframe>

    <script>
        document.getElementById('myIframe').onload = function() {
            var iframeWindow = this.contentWindow;
            var messageData = {
                action: 'updateData',
                data: {
                    id: 42,
                    name: 'Alice'
                }
            };
            iframeWindow.postMessage(JSON.stringify(messageData), 'http://receiver.example.com');
        };
    </script>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>接收消息页面</title>
  </head>
  <body>
    <script>
      window.addEventListener("message", receiveMessage, false);
      function receiveMessage(e) {
        console.log(e.data);
        console.log(e);
        // window.parent.postMessage("嘿嘿嘿嘿", "*"); // 发送给任意窗口
        // e.source.postMessage("嘿嘿嘿嘿", "http://localhost:3000");
        e.source.postMessage("嘿嘿嘿嘿", e.origin);
      }
    </script>
  </body>
</html>

3.4 window.name

window.name 跨域解决方案是一种较旧的技术,它利用了浏览器的一个特性:window.name 属性的值在页面跳转后依然会被保存,而且这个属性可以存储大量的数据(理论上可达几兆字节),同时不受同源策略的限制。

// a.html
<body>
  <iframe
    src="http://localhost:4000/c.html"
    id="frame"
    onload="load()"
    ></iframe>
  <script>
    let first = true;
    function load() {
      if (first) { // 修改 src后 会重新执行 load方法
        let frame = document.getElementById("frame");
        frame.src = "http://localhost:3000/b.html";
        first = false;
        console.log(frame.contentWindow.name) // 报跨域错误 
      } else {
        console.log(frame.contentWindow.name); // 可以正常拿到值
      }
    }
  </script>
</body>
<body>
  <script>
    window.name = "I love you";
  </script>
</body>

3.5 location.hash

location.hash 跨域是一种利用URL的哈希部分(即URL中 #后面的部分 )来实现跨域信息传递的技巧。由于浏览器的同源策略限制,直接通过脚本访问不同源的页面数据是被禁止的,但 location.hash 的修改不会触发页面刷新,同时浏览器允许修改不同源iframe的 location.hash,因此可以作为一种跨域通信的手段。

  1. 源页面(域A)通过动态创建一个隐藏的 <iframe>,并将该 iframesrc 属性设置为目标页面(域B)的URL。同时,在这个URL后面加上特定的 hash 值,用来携带要传递的信息。例如,http://target.com/page.html#someData
  2. 目标页面(域B)的JavaScript代码会监听自身的hash变化。当iframe加载完成并且源页面设置了新的hash值时,目标页面能够捕获到这个变化。
  3. 目标页面解析hash中的信息,并根据需要处理这些数据。处理完毕后,如果需要向源页面反馈信息,目标页面同样可以修改自身的hash值,源页面通过同样的监听机制可以获取到这些反馈信息。

3.6 document.domain

document.domain 跨域是一种较为早期的技术手段,用于解决同一二级域名下的页面之间因浏览器同源策略限制而无法直接通信的问题。这种方法允许两个页面通过设置它们的 document.domain 属性为相同的值来绕过同源策略的一部分限制,实现一定程度上的跨域交互。

  1. 此方法仅适用于同一顶级域名下的不同子域名之间。例如,a.example.comb.example.com 可以通过此方法实现跨域通信,但 example.comanotherexample.com 则不行。
  2. 在两个想要相互通信的页面中,都必须通过JavaScript设置它们的 document.domain 属性为相同的值,通常是它们的共同上级域名。
  3. 一旦两个页面的 document.domain 设置相同,它们就可以通过JavaScript访问彼此的DOM,从而实现数据的传递和交互。

3.7 WebSocket

WebSocket本身设计上不受到浏览器同源策略的限制,因此天然支持跨域通信。这意味着你可以从一个域名发起WebSocket连接到另一个域名的服务器,而不会像Ajax那样受到同源策略的阻拦。

3.8 Nginx

Nginx 是一个强大的 Web 服务器和反向代理服务器,它可以通过配置来解决前端开发中常见的跨域问题。

server {
    listen 80;
    server_name your.domain.com;

    location /api/ {
        # 如果你的后端服务也在 Nginx 中,则在此处转发请求
        # proxy_pass http://backend_service;

        # 添加跨域相关的响应头
        proxy_set_header X-Real-IP $remote_addr; # 设置 X-Real-IP 头信息,包含客户端的真实IP 地址
 				proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 设置 XForwarded-For 头信息,包含所有中间代理的 IP 地址
 				proxy_set_header X-Forwarded-Proto $scheme; # 设置 X-Forwarded-Proto 头信息,包含客户端请求使⽤的协议(HTTPHTTPS        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        add_header 'Access-Control-Max-Age' 1728000;

        # 对于预检请求(OPTIONS)直接返回204状态码
        if ($request_method = 'OPTIONS') {
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # 如果是代理到其他服务,请取消注释下面的proxy_pass行,并去掉以下注释
        rewrite ^/api/(.*)$ /$1 break; # 因为服务器没有/api,所以做⼀次重写
        proxy_pass http://localhost:8000; # 代理到api服务器
    }
}

3.9 vite proxy

Vite 的 proxy 功能实现跨域的原理基于 Node.js 的 HTTP 代理中间件,它在开发服务器(dev server)上建立了一个代理层。

  1. 请求拦截:当前端应用运行在 Vite 开发服务器上,并发起一个 HTTP 请求时,如果该请求的 URL 匹配了 vite.config.jsserver.proxy 配置的代理规则,Vite 的开发服务器并不会直接将请求发送到浏览器指定的原始地址,而是拦截这个请求。

  2. 请求转发:拦截后的请求会被 Vite 重定向(或称为“代理”)到你在配置中指定的 target 地址。这意味着,Vite 开发服务器实际上扮演了一个代理服务器的角色,它接收到前端的请求后,会根据代理规则,将请求转发给真正的后端服务器。

  3. 源站伪装:在转发过程中,Vite 可以通过设置 changeOrigin 选项来改变请求的源(Origin),使得后端服务器认为请求是直接从代理服务器发出的,而非前端客户端。这一操作绕过了浏览器的同源策略限制,因为从后端服务器的角度看,请求是同源的。

  4. 路径重写:在某些情况下,你可能需要对请求的路径进行调整,以匹配后端服务器的预期。这时可以使用 rewrite 函数来修改请求路径。

  5. 响应接收与转发:后端服务器处理完请求后,将响应返回给 Vite 的代理服务器,代理服务器再将这个响应转发回前端应用。由于 Vite 服务器和前端运行在同一源下,这个响应被视为同源的,因此不会被浏览器的同源策略所阻止。