浏览器 跨域传输数据、标签页传输数据

同源策略

  • 同源指的是我们访问站点的:协议、域名、端口号必须一至,才叫同源。
  • 浏览器默认同源之间的站点是可以相互访问资源和操作DOM的,而不同源之间想要互相访问资源或者操作DOM,那就需要加一些安全策略的限制,俗称同源策略
  • 同源策略主要限制了三个方面:
    • DOM层面:不同源站点之间不能相互访问和操作DOM
    • 数据层面:不能获取不同源站点的Cookie、LocalStorage、indexDB等数据
    • 网络层面:不能通过XMLHttpRequest向不同源站点发送请求

不受跨域限制的内容

  • img:可以在网站上使用来自其他来源的 标签显示图片。但不能读取其他域的图片像素信息,因为这仍然会触发跨域限制。
  • iframes:你可以在你的网站中嵌入来自其他域名的内容。然而,你不能使用 JavaScript 访问跨域 iframe 内部的 DOM,除非使用了跨域资源共享(CORS)策略和 <iframe> 标签中的 sandbox 属性
  • script:可以使用跨域的 <script> 标签引用外部 JavaScript 文件
    • 外部 js 文件可以引入并加载,但是脚本默认是不允许访问其它域名下的资源,所以还是需要设置 Access-Control-Allow-Origin
  • link:可以使用跨域的 <link> 标签引用外部 CSS 样式表文件
  • <video> 和 <audio> :对于媒体元素(如视频和音频)的 API 操作仍然受到跨域限制。

小程序有没有跨域一说

  • 在小程序中的跨域问题主要涉及到网络请求和 WebSocket。需要在小程序的管理后台设置相关的安全域名、上传文件域名、下载文件域名和 WebSocket 域名。只有经过配置的域名才可以在小程序中发起相应的请求,并且只支持 HTTPS 请求以及 wss(WebSocket Secure)协议
  • 小程序并没有采用相同于浏览器环境的CORS(跨域资源共享)机制。在进行后端接口设计时,无需对响应头设置 Access-Control-Allow-Origin 等相关字段以适应 CORS 限制,只需要在小程序管理后台配置好对应域名即可

跨域传输数据

CORS 跨域

CORS需要浏览器和服务器同时支持

服务器端:

  • Access-Control-Allow-Origin:指定哪些源可以访问该资源。可以设置为特定的源(例如:http://example.com),也可以设置为通配符 * 以允许所有来源的访问。

  • Access-Control-Allow-Methods:列出服务器允许的 HTTP 方法(如 GET, POST, PUT, DELETE)。允许多个方法用逗号分隔。

  • Access-Control-Allow-Headers:指定允许携带哪些自定义请求头来访问该资源,例如:X-Requested-With, Content-Type 等。允许多个请求头用逗号分隔。

  • Access-Control-Max-Age:定义一个时间,表示服务器的响应可以在客户端被缓存多久。值为秒数。

  • Access-Control-Allow-Credentials:设为 true,则表示允许请求携带认证信息(如 cookies)进行跨域访问。默认情况下,跨域请求不会发送认证信息。

  • Access-Control-Expose-Headers:列出哪些头信息会被客户端访问到。这一头部字段提供了一份白名单,只有在此列出的响应头部,客户端才能访问。

浏览器端

  • Access-Control-Request-Method:这是浏览器在预检请求(preflight request)中发送的头部,用于告诉服务器该跨域请求将使用的 HTTP 方法。

  • Access-Control-Request-Headers:这是浏览器在预检请求中发送的头部,用于告诉服务器该跨域请求将携带的自定义 HTTP 请求头部。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)

详情

proxy 代理服务器

  • 通过 Nginx 配置一个代理服务器域名与浏览器的 domain1 相同,端口不同;通过配置 Nginx 反向代理,可以将来自不同端口的请求都代理到同一个端口,从而消除跨域问题,然后将请求转发到目标服务器,接收服务器的响应数据并将其返回给客户端,实际上客户端并不直接与目标服务器进行交互,只和代理服务器交互。
location / {
  add_header Access-Control-Allow-Origin *;
}

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  // 反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; // 修改cookie里域名
        index  index.html index.htm;

        // 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  // 当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

WebSocket

  • WebSocket 不受浏览器同源策略的限制,对于 WebSocket 的跨域检测工作就交给了服务端,浏览器仍然会带上一个 Origin 跨域请求头,服务端则根据这个请求头判断此次跨域 WebSocket 请求是否合法

iframe + document.domain

  • 仅限主域相同,子域不同的场景,两个页面都通过js设置document.domain为基础主域
//父域 http://www.domain.com/a.html
<iframe id='iframe' src='http://child.domain.com/b.html' />

document.domain='domain.com'
window.user='user'

//子域 http://child.domain.com/b.html
document.domain='domain.com'
window.parent.user

iframe + window.name

  • window.name 的值在 iframe 导航过程中会保持不变,并且 name 支持2M的数据
  • 在父域页面 A.html 内嵌了一个子域页面 B.html 的 iframe。当这个 iframe 完全载入完成后,将它的 src 改为同父域的一个中间页面 middle.html。
  • 因此在中间页面 middle.html 中可以读取到之前子域页面 B.html 设置的 window.name 数据。然后,将这个数据通过 postMessage 传递给 A.html 页面。这样,我们实现了跨域传输数据的目的。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>A 页面 - 父域名</title>
</head>
<body>
    <h1>这是页面 A,属于 parent.example.com</h1>
    <iframe id="bridge" src="http://child.example.com/B.html" style="display: none;"></iframe>
    <script>
        window.onload = function() {
            const bridgeIframe = document.getElementById('bridge');
            bridgeIframe.onload = function() {
                // 当子域页面载入完成后,将 iframe 的 src 设置为同域的中间页面
                bridgeIframe.src = 'middle.html';
            }
        };

        window.addEventListener('message', function(event) {
            if (event.origin !== 'http://parent.example.com') return;
            console.log("接收到跨域的数据:", event.data);
        });
    </script>
</body>
</html>
// 子域(child.example.com)的页面 B.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>A 页面 - 父域名</title>
</head>
<body>
    <h1>这是页面 A,属于 parent.example.com</h1>
    <iframe id="bridge" src="http://child.example.com/B.html" style="display: none;"></iframe>
    <script>
        window.onload = function() {
            const bridgeIframe = document.getElementById('bridge');
            bridgeIframe.onload = function() {
                // 当子域页面载入完成后,将 iframe 的 src 设置为同域的中间页面
                bridgeIframe.src = 'middle.html';
            }
        };

        window.addEventListener('message', function(event) {
            if (event.origin !== 'http://parent.example.com') return;
            console.log("接收到跨域的数据:", event.data);
        });
    </script>
</body>
</html>
// 中间页面 middle.html (与 A.html 同域,即 parent.example.com):
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>中间页面 - middle.html</title>
</head>
<body>
    <script>
        const data = window.name;
        window.parent.postMessage(data, 'http://parent.example.com');
    </script>
</body>
</html>

iframe + hash

// http://www.domain1.com/a.html
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // http://www.domain2.com/b.html
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
</script>

// http://www.domain2.com/b.html

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

iframe + postMessage

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 同样监听message 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

// 监听message  接收domain1的数据
window.addEventListener('message', function(e) {
  alert('data from domain1 ---> ' + e.data);

  var data = JSON.parse(e.data);
  if (data) {
    data.number = 16;
    // 处理后再发回domain1
    window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
  }
}, false);

JSONP

实现原理

  • 首先是利用 script 标签的 src 属性来实现跨域
  • 然后通过全局函数进行调用
// 注册全局函数
window[jsonpCallback_1629198396834_895]=function(params){}

// 前端发送请求
https://your-jsonp-url?callback=jsonpCallback_1629198396834_895

// 服务端响应返回 js 代码
// 服务端需要返回一个 Content-Type 类型为 'application/javascript' 的响应头
// 返回内容
jsonpCallback_1629198396834_895({
  "data": "This is the data from the server."
});

  • 优点
    • JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
  • 缺点
    • 由于使用script标签的src属性,因此只支持get方法
    • 存在 xss 注入的风险,比如拼接的是 alert
    • 如果第三方服务器端返回了恶意代码,客户端的安全性可能会受到威胁
    • 错误捕捉困难:JSONP 没有原生的错误处理机制,当请求失败(例如服务器无响应或返回非法的 JSONP 格式数据)时,script 标签无法捕捉到这些错误

JSONP并发冲突

  • 两个几乎同时发起的请求可能会共享同一个回调函数,导致数据覆盖和混乱。
  • 可以为每个 JSONP 请求指定唯一的回调函数,以避免冲突。

cookie + document.domain

  • 通过设置 document.domain 可以让父域和子域共享的 cookie
  • 子域读取父域
    • 在父域名 example.com 页面设置 cookie 时,可以指定domain=.example.com,这样父域名和所有子域名将共享此cookie,子域名可以正常读取
document.cookie = "key=value; domain=.example.com; path=/";
  • 父域读取子域
    • 在子域名 a.example.com 的页面设置一个可以被父域名 example.com 读取的cookie,可以这样设置
document.cookie = "key=value; domain=example.com; path=/";

标签页之间传输数据

同源

localStorage

BroadcastChannel

详情

SharedWorker

ServiceWorker

  • 通过 postMessage 和 clients.matchAll 方法,在多个同源标签页之间传递消息。
// sw.js - Service Worker 文件
self.addEventListener('message', (event) => {
    // 发送消息给所有的clients(包括标签页、受控页面等)
    event.waitUntil(self.clients.matchAll().then((clients) => {
      clients.forEach((client) => {
        // 发给其他标签页
        if (client.id !== event.source.id) {
          client.postMessage({
            data: event.data,
            srcClientId: event.source.id
          });
        }
      });
    }));
});
<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js').then(() => {
      console.log('Service Worker 注册成功');
    }).catch((error) => {
      console.error('Service Worker 注册失败:', error);
    });
  }

  navigator.serviceWorker.addEventListener('message', (event) => {
    console.log('接收到来自 Service Worker 的信息:', event.data);
  });
  
  // 发送给 ServiceWorker
  function sendDataToOtherTabs(data) {
    navigator.serviceWorker.controller.postMessage(data);
  }
</script>
</body>
</html>

源标签页打开窗口 + window.opener.postMessage

  • 如果标签页是通过<a target=‘_blank’> 、window.open 打开的,可以使用 window.opener.postMessage 方法进行通信。

HTTP 请求

websocket

cookie

不同源

WebSocket

window.open + window.opener

  • window.opener 为打开其它窗口的窗口
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>A 页面 - a.example.com</title>
</head>
<body>
    <h1>这是页面 A,属于 a.example.com</h1>
    <button id="open">打开 B 页面</button>
    <script>
        document.getElementById('open').addEventListener('click', function() {
            const newWindow = window.open('http://b.example.com/B.html');
        });

        window.addEventListener('message', function(event) {
            if (event.origin !== 'http://b.example.com') return;
            console.log('接收到跨域数据:', event.data);
        });
    </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>B 页面 - b.example.com</title>
</head>
<body>
    <h1>这是页面 B,属于 b.example.com</h1>
    <button id="send">发送数据给 A 页面</button>
    <script>
        document.getElementById('send').addEventListener('click', function() {
            window.opener.postMessage('我是来自页面 B 的数据', 'http://a.example.com');
        });
    </script>
</body>
</html>

cookie

  • 父域和子域
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值