同源策略
- 同源指的是我们访问站点的:协议、域名、端口号必须一至,才叫同源。
- 浏览器默认同源之间的站点是可以相互访问资源和操作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
- 父域和子域