跨域问题的处理
什么是跨域?
同源策略控制了不同源之间的交互,例如在使用XMLHttpRequest或标签时则会受到同源策略的约束。这些交互通常分为三类:
- 通常允许跨域写操作(Cross-origin writes)。例如链接(links),重定向以及表单提交。
- 通常允许跨域资源嵌入(Cross-origin embedding)。
- 通常不允许跨域读操作(Cross-origin reads)。但常可以通过内嵌资源来巧妙的进行读取访问。
下面为允许跨域资源嵌入的示例,即一些不受同源策略影响的标签示例:
- <script src="…"></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。
- <link rel=“stylesheet” href="…">标签嵌入CSS。
- <img>嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG。
- <video> 和 <audio>嵌入多媒体资源。
- <frame>和<iframe>载入的任何资源。
同源策略
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。浏览器的同源策略的目的就是为了防止 XSS,CSRF 等恶意攻击。
只有资源之间的协议,域名和端口号都相同,才是同一个源。
跨域解决方案
1. JSONP
由于浏览器同源策略是允许 script 标签这样的跨域资源嵌套的,所以 script 标签的资源不受同源策略的限制。JSONP 的解决方案就是通过 script 标签进行跨域请求,由其 src 属性发送请求到服务器,服务器返回 JavaScript 代码,浏览器接受响应,然后就直接执行了,这和通过 script 标签引用外部文件的原理是一样的。
JSONP由两部分组成:回调函数
和数据
,回调函数一般是在浏览器控制,作为参数发往服务器端当服务器响应时,服务器端就会把该函数和数据拼成字符串返回。
JSONP的请求过程:
- 请求阶段:浏览器创建一个 script 标签,并给其src 赋值(类似 http://example.com/api/?callback=jsonpCallback )。
- 发送请求:当给script的src赋值时,浏览器就会发起一个请求。
- 数据响应:服务端将要返回的数据作为参数和函数名称拼接在一起(格式类似”jsonpCallback({name: ‘abc’})”)返回。当浏览器接收到了响应数据,由于发起请求的是 script,所以相当于直接调用 jsonpCallback 方法,并且传入了一个参数。
function jsonp({url, param, cb}){
return new Promise((resolve, reject)=>{
let script = document.createElement('script')
window[cb] = function(data){
resolve(data);
document.body.removeChild(script)
}
params = {...params, cb}
let arrs = [];
for(let key in params){
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/say',
params: {wd: 'haoxl'},
cb: 'show'
}).then(data=>{
console.log(data)
})
优缺点:
- 兼容性好,低版本的 IE 也支持这种方式。
- 只能支持 GET 方式的 HTTP 请求。
2. CORS
CORS 跨域资源共享允许在服务端进行相关设置后,可以进行跨域通信。
服务端未设置 CORS 跨域字段,服务端会拒绝请求并提示错误警告。
服务端设置 Access-Control-Allow-Origin 字段,值可以是具体的域名或者 ‘*’ 通配符,配置好后就可以允许跨域请求数据。
3.location.hash + iframe
location.hash + iframe 跨域通信的实现是这样的:
- 不同域的 a 页面与 b 页面进行通信,在 a 页面中通过 iframe 嵌入 b 页面,并给 iframe 的 src 添加一个 hash 值。
- b 页面接收到了 hash 值后,确定 a 页面在尝试着与自己通信,然后通过修改 parent.location.hash 的值,将要通信的数据传递给 a 页面的 hash 值。
- 但由于在 IE 和 Chrmoe 下不允许子页面直接修改父页面的 hash 值,所以需要一个代理页面,通过与 a 页面同域的 c 页面来传递数据。
- 同样的在 b 页面中通过 iframe 嵌入 c 页面,将要传递的数据通过 iframe 的 src 链接的 hash 值传递给 c 页面,由于 a 页面与 c 页面同域,c 页面可以直接修改 a 页面的 hash 值或者调用 a 页面中的全局函数。
大致流程:
a 页面:
<script>
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = "http://localhost:8081/b.html#data";
document.body.appendChild(iframe);
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
console.log('获得到的数据是:', data);
}catch(e) {}
}
window.addEventListener('hashchange', function(e) {
console.log('监听到hash的变化:', location.hash.substring(1));
})
</script>
b页面:
<script>
switch(location.hash) {
case '#data':
callback();
break;
}
function callback() {
var data = "testHash"
try {
parent.location.hash = data;
}catch(e) {
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://localhost:8080/c.html#' + data;
document.body.appendChild(ifrproxy);
}
}
</script>
c页面:
<script>
// 修改 a 页面的 hash 值
parent.parent.location.hash = self.location.hash.substring(1);
// 调用 a 页面的全局函数
parent.parent.checkHash();
</script>
优缺点:
- hash 传递的数据容量有限。
- 数据直接暴露在 url 中。
4.window.name + iframe
window.name 指的是当前浏览器窗口的名称,默认为空字符串,每个窗口的 window.name 都是独立的。iframe 嵌套的页面中也有属于自己的 window 对象,这个 window 是top window 的子窗口,也同样拥有 window.name 的属性。
window.name 的独特之处在于当在页面设置 window.name 的值,其实就是相当于给这个窗口设置了名称,而后在这个窗口加载其他页面(甚至不同域的页面),window.name 的值依然存在(如果没有重新设置那么值不会变化),并且 window.name 的值支持比较大的存储(2MB)。
例如: 随便找个页面打开控制台,给当前窗口设置名称。
window.name = 'test-name';
设置好之后可以在这个窗口下跳转到其他页面。
window.location = 'https://www.baidu.com';
页面跳转到了百度首页,但是 window.name 的值依然是之前设置的值,因为是在一个窗口中跳转的页面,窗口名称并不会被修改。具体的跨域解决方式如下:
http://localhost:8080/a.html 与 http://localhost:8081/b.html 跨域通信,a 页面通过 iframe 嵌套 b 页面,b 页面中设置好 window.name 的值,由于是不同域,a 页面不能直接访问到 b 页面设置的 window.name 的值,需要一个与 a 页面同域的中间页来代理作为 a 页面与 b 页面通信的桥梁。
a页面
<script>
var data = null;
var state = 0;
var iframe = document.createElement('iframe');
iframe.src = "http://localhost:8081/b.html";
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 第一次加载先加载 b.html,b.html 设置好了 window.name 的值
// 而后加载 c.html,c.html 的 window.name 的值就是之前 b.html 设置的值
// 同域的情况下,a.html 可以通过 iframe.contentWindow.name 获取到 b.html 中 windoa.name 的值
iframe.onload = function() {
if(state === 0) {
iframe.src = "http://localhost:8080/c.html";
state = 1;
}else if(state === 1) {
data = iframe.contentWindow.name;
console.log('收到数据:', data);
}
}
</script>
b页面
<script>
window.name = '这是传递的数据';
</script>
5.window.postMessage
postMessage 方法接受两个必要的参数:
- message: 需要传递的数据。
- targetOrigin: 数据传递的目标窗口域名,值可以是具体的域名或者 ‘*’ 通配符。
a页面
<iframe src="http://localhost:8081/b.html" style='display: none;'></iframe>
<script>
window.onload = function() {
var targetOrigin = 'http://localhost:8081';
var data = {
name: 'lee',
};
// 向 b.html 发送消息
window.frames[0].postMessage(data, targetOrigin);
// 接收 b.html 发送的数据
window.addEventListener('message', function(e) {
console.log('b.html 发送来的消息:', e.data);
})
}
</script>
b页面
<script>
var targetOrigin = 'http://localhost:8080';
window.addEventListener('message', function(e) {
if(e.source != window.parent) {
return;
}
// 接收 a.html 发送的数据
console.log('a.html 发送来的消息:', e.data);
// 向 a.html 发送消息
parent.postMessage('哈哈,我是b页面,我收到你的消息了', targetOrigin);
})
</script>
6.websocket
WebSocket对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。它是基于TCP的全双工通信,即服务端和客户端可以双向进行通讯,并且允许跨域通讯。基本协议有ws://(非加密)和wss://(加密)
//socket.html
let socket = new WebSocket('ws://localhost:3000');
// 给服务器发消息
socket.onopen = function() {
socket.send('hello server')
}
// 接收服务器回复的消息
socket.onmessage = function(e) {
console.log(e.data)
}
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//npm i ws
// 设置服务器域为3000端口
let wss = new WebSocket.Server({port:3000});
//连接
wss.on('connection', function(ws){
// 接收客户端传来的消息
ws.on('message', function(data){
console.log(data);
// 服务端回复消息
ws.send('hello client')
})
})