同源策略
同源策略(Same-Origin Policy),是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的很多功能都无法正常使用。 "同源" 指的是 "协议 + 域名 + 端口
" 三者相同。 同源策略的主要目标是为了保护用户的信息安全,防止恶意的网站访问和窃取其他网站的数据。
当前页面url | 被请求页面url | 跨域 | 原因 |
---|---|---|---|
http://www.test.com/ | http://www.test.com/index.html | 否 | 同源(协议、域名、端口号相同) |
http://www.test.com/ | https://www.test.com/index.html | 跨域 | 协议不同(http/https) |
http://www.test.com/ | http://www.baidu.com/ | 跨域 | 主域名不同(test/baidu) |
http://www.test.com/ | http://blog.test.com/ | 跨域 | 子域名不同(www/blog) |
http://www.test.com:8080/ | http://www.test.com:7001/ | 跨域 | 端口号不同(8080/7001) |
那些行为受到跨域的限制?
- Cookie、LocalStorage 和 IndexDB 不能进行跨域访问。(访问存储在浏览器中的数据,如 localStorage 和 IndexedDB,是以源进行分割。每个源都拥有自己单独的存储空间)
- DOM 无法获取到其他页面的对象
- AJAX 请求不能发送到不同源的地址,除非它支持 CORS(跨源资源共享)
Ajax为什么不能跨域访问
Ajax 其实就是向服务器发送一个 GET 或 POST 请求,然后取得服务器响应结果,返回客户端。
Ajax 跨域请求,在服务器端不会有任何问题。 在客户端,实际上,请求会正常发送到服务器,服务器也会正常返回响应。 只是服务端响应数据返回给浏览器的时候,浏览器根据响应头的Access-Control-Allow-Origin
字段的值来判断是否有权限获取数据,服务端如果没有设置跨域字段设置,跨域请求就没有权限访问,数据被浏览器给拦截了。
跨域的解决方案
- 跨域资源共享CORS
- JSONP(只支持get)
- postMessage跨域
- nginx代理跨域
- nodejs中间件代理跨域
CORS
跨域资源共享CORS
,是一种 W3C 标准,允许服务器在响应头中加入特殊字段,如 "Access-Control-Allow-Origin",来开放对指定来源的跨域请求。这是最标准也最通用的跨域解决方案。
CORS请求步骤
- CORS 请求一般分为两种:
简单请求和复杂请求
。 - 当我们发起跨域请求时,如果是复杂请求,浏览器会帮我们自动触发预检请求,也就是
OPTIONS
请求,用于确认目标资源是否支持跨域。如果是简单请求,则不会触发预检,直接发出正常请求。 - 浏览器会根据服务端响应的
header(Access-Control-Allow-origin)
进行判断,如果响应支持跨域,则继续发出正常请求,如果不支持,则在控制台显示错误。
简单请求
满足一下条件,则可视为简单请求,不会触发CORS预检请求
使用下列方法之一:
- GET
- HEAD
- POST
除了被用户代理自动设置的请求头(connecttion,User-Agent)允许人为设置的字段只能是下面几种
Content-Type只能是下面三种
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
其他一些条件
简单请求的流程主要有以下两个步骤:
- 客户端直接发出CORS请求,就是正常的XMLHttpRequest请求,添加了Origin头信息。
- 服务器返回的响应,会多出几个头信息Access-Control-Allow-Origin,这表示服务器接受这次跨源请求。
复杂请求
凡是不同时满足上面两个条件,就属于非简单请求(复杂请求)。 复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求 ,该请求的方法是 Option,通过该请求来查询服务端是否允许跨域请求。
预检请求的流程主要有以下步骤:
- 首先,浏览器会向服务器发送一次预检请求,这是一种 OPTIONS 请求,其中包含了 CORS 相关请求头信息,比如
Origin
(标明这个请求发出的域),Access-Control-Request-Method(用来让服务器判断实际发出请求将用什么方法,是否允许, 此标头必须)。 - 如果服务器允许这次请求,那么就会返回一个包含了
Access-Control-Allow-*
头的响应。这些头信息表明了服务器允许的源、方法、自定义请求头等信息。- Access-Control-Allow-Origin: 能够被允许发出这个请求的域名,也可以使用*来表明允许所有域名;
- Access-Control-Allow-Methods: 用逗号分隔的被允许的请求方法的列表;
- Access-Control-Allow-Headers: 用逗号分隔的被允许的请求头部字段的列表;
- Access-Control-Max-Age: 这个预检请求能被缓存的最长时间,在缓存时间内,同一个请求不会再次发出预检请求。
- 浏览器判断预检响应是有效的(即服务器允许跨域请求)后,就会发出实际的 AJAX 请求。
- 服务器对这次请求再次进行响应,如果响应头部中的 Access-Control-Allow-Origin 允许源的请求,那么前端就可以接收到响应数据。
跨域请求如何携带Cookie
- 服务器的
Access-Control-Allow-Origin 设置为* 或者对应的域名
; - 服务器的响应头中
**Access-Control-Allow-Credentials: true**
- 浏览器SameSite要为none
- Request 请求设置withCredentials为true(不是请求头设置)
聊聊withCredentials
withCredentials
不是在请求头中设置的,而是作为请求对象 XMLHttpRequest 或 fetch 的一个属性设置的 在跨域请求中,如果想要包含 Cookie 或者 HTTP 认证信息,需要设置 withCredentials 属性为 true
对于 XMLHttpRequest:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com', true);
xhr.withCredentials = true; // 设置 withCredentials 属性
xhr.send();
对于 fetch API:
fetch('http://example.com', {
credentials: 'include' // 这等同于设置 withCredentials = true
});
设置这个属性后,跨域的 AJAX 请求将会带上源站点的数据(包括 Cookie 和 HTTP 认证信息)。
JSONP
JSONP 的原理很简单,就是利用 <script>
标签没有跨域限制的漏洞。通过 <script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据。
- JSONP只支持get方法
- 6种解决跨域方案,今天全告诉你了-腾讯云开发者社区-腾讯云
<script src="http://xxxxxx&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
postMessage跨域
window.postMessage() 方法可以安全地实现跨源通信。我们只需要拥有另一个窗口的引用
,就可以传递消息给另一个窗口;
从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener
),然后在窗口上调用 targetWindow.postMessage()
方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件(en-US)。传递给 window.postMessage() 的参数(比如 message)将通过消息事件对象暴露给接收消息的窗口。
语法
otherWindow.postMessage(message, targetOrigin, [transfer])
otherWindow
:其他窗口的一个引用,比如
- iframe 的 contentWindow 属性、
- 执行window.open返回的窗口对象、
- 或者是命名过或数值索引的window.frames (en-US)。
也就是说,postMessage可以解决以下访问的问题
- 页面和页面上打开的新窗口的数据传递(使用window.open打开的)
- 页面与嵌套的 iframe 消息传递
举例
我们假设有两个页面:父页面 Parent.html 和在父页面中的一个<iframe>
子页面 Child.html
- 引用子页面 Child.html 的父页面 Parent.html
<!DOCTYPE html>
<html>
<body>
<iframe id="iframe" src="https://child.com/Child.html" style="display:none;"></iframe>
<button onClick="postMessageToIframe()">Post Message to Iframe</button>
<script>
function postMessageToIframe() {
const iframe = document.getElementById('iframe');
iframe.contentWindow.postMessage('Hello Child', 'https://child.com');
}
</script>
</body>
</html>
在上述代码中,父页面通过 postMessage 方法向子页面发送了消息 Hello Child
- 子页面Child.html位于另一个域:
<!DOCTYPE html>
<html>
<body>
<script>
window.addEventListener('message', function(event) {
if (event.origin !== 'http://parent.com') return;
console.log('Received message:', event.data);
event.source.postMessage('Hello Parent', event.origin);
}, false);
</script>
</body>
</html>
子页面侦听 message 事件以接收父页面发送过来的消息,然后检查发送消息的源是否与期望的源一致,如果一致,则处理这条消息,并向发送消息的窗口回发一条消息 Hello Parent
Nignx Proxy方案
在实际的开发中,通过 Nginx 代理的方式是非常常见的解决跨域的方案之一。 原理是,由于浏览器执行 Javascript 的安全策略仅放行同源(即相同的域名、端口,协议)的请求,所以我们可以在前端服务器上设置一个代理,将请求先发给自家的服务器,再由服务器去请求其他源的数据,然后返回给浏览器,这样浏览器的同源策略就会认为没有问题了。
server {
listen 80;
server_name mydomain.com;
location /api/ {
proxy_pass http://otherdomain.com; # 另一个域名
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
在上面配置里,所有前端发送至 'mydomain.com/api
' 的请求都会被 Nginx 服务器转发给 'otherdomain.com'。
- 请求头的 'Host' 被重写为当前 Nginx 服务器的地址,
- 而 'X-Real-IP' 和 'X-Forwarded-For' 则会被添加到请求头中,方便调试跟踪源的 IP 地址
返回给前端时,服务器也会对响应做出一些修改,前端得到的仍然是一个看起来像是从 Nginx 代理服务器上返回的结果,而不是直接从其他源获取的结果,因此不会产生跨域问题。
再比如这样一个配置
server {
listen 80;
server_name client.com;
location /api {
proxy_pass server.com;
}
}
Nginx 相当于起了一个中转站,这个中转站的域名也是client.com,让客户端首先访问 client.com/api,这当然没有跨域,然后 Nginx 服务器作为反向代理,将请求转发给server.com,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。
跨页签通信 & 多tab通信
localStorage/sessionStorage
一个页面修改了 LocalStorage/SessionStorage,其他页签可以通过监听'storage'
事件获得新的数据。但需要注意的是,这种方式只有在页签之间共享相同源(同域名)的情况下才可以使用。
- 重复写入相同的值无法触发
- 会受到浏览器隐身模式等的限制
// 页面 A
localStorage.setItem('myKey', 'myValue');
// 页面 B
window.addEventListener('storage', function(event) {
if (event.key == 'myKey') {
console.log(event.newValue);
}
});
postMessage
- 通过父页面window.open()和子页面postMessage
虽然postMessage一般用于iframe和宿主页面之间的通信,但也可以用于其他窗口或页签。
// 页面 A
const win = window.open('http://example.com/pageB.html');
win.postMessage('Hello!', 'http://example.com');
// 页面 B
window.addEventListener('message', function(event) {
if (event.origin !== 'http://example.com') return;
console.log(event.data);
}, false);
BroadcastChannel
是一个比较新的API,它提供了一个简单的方法,可以在同源的不同页面(包括iframe)之间进行通信。
// 页面 A
const bc = new BroadcastChannel('test_channel');
bc.postMessage('This is a message from page A.');
// 页面 B
const bc = new BroadcastChannel('test_channel');
bc.onmessage = function (event) { console.log(event.data); };
SharedWorker
SharedWorker 是 Web Workers 的一种,可以同一源下的多个页面共享。 SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域.
如果要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口)。
// 页面 A
const worker = new SharedWorker('worker.js');
worker.port.postMessage('This is page A.');
// 页面 B
const worker = new SharedWorker('worker.js');
worker.port.onmessage = function(event) {
console.log(event.data);
};
WebSocket配合服务器
WebSocket可以配合服务器实现多个浏览器tab之间的通信。 当用户开启一个新的tab并建立WebSocket连接时,服务器可以通过唯一的用户ID或session来识别这个用户。 当服务器接收到特定用户推送的消息时,可以将消息广播到所有该用户ID的活动连接。 这样,当用户在一个tab页面中进行操作,其他所有tab页面都可以接收到这个操作的通知。 客户端:
// 创建一个WebSocket连接
const socket = new WebSocket('ws://example.com/socketserver');
socket.addEventListener('message', function (event) {
console.log('收到消息: ', event.data);
});
// 在需要的时候,向服务器发送消息
socket.send('你好,我是客户端');
服务器端
const WebSocket = require('ws');
// 初始化Websocket服务器
const wss = new WebSocket.Server({ port: 8080 });
let connections = {};
wss.on('connection', (ws) => {
ws.on('message', (message) => {
// 假设消息的结构如下:{userId:"xxx", content:"xxx"}
let msg = JSON.parse(message);
if(!connections[msg.userId]) connections[msg.userId] = []; // 如果不存在该用户的连接数组,初始化一个
connections[msg.userId].push(ws); // 将新ws连接添加到该用户的连接数组
// 广播该用户所有的连接
connections[msg.userId].forEach(conn => conn.send(msg.content));
});
});