1. 跨域问题
广义上的跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,例如:
- 资源跳转:a链接、重定向、表单提交;
- 资源嵌入:<link>、<script>、<img>、<frame>等dom标签,还有样式中background:url()、@font-face()等文件外链;
- 脚本请求:js发起的ajax请求、dom和js对象的跨域操作等
而我们通常所说的跨域可以理解为狭义上的跨域,是由浏览器同源策略限制的一类请求场景。即 浏览器施加了安全限制--即同源策略,不能执行其他网站页面脚本,从而产生了跨域问题。
2. 同源策略(SOP:Same origin policy)
同源策略是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能。同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。如果缺少了同源策略,浏览器很容易收到XSS、CSFR等攻击。
同源是指“协议+域名+端口”三者完全相同,且子域名也必须相同,例如存在页面http://www.domain.com/a.html,想访问另一个b.html页面,则:
b.html页面的URL | 说明 | 是否跨域 |
http://www.domain.com/b.html | 同协议、同域名、同端口,可通信 | 否 |
http://www.domain.com/file/b.html | 同协议、同域名、同端口,不同文件夹,可通信 | 否 |
https://www.domain.com/b.html | 不同协议、同域名、同端口,不可通信 | 是 |
http://w.domain.com/b.html http://domain.com/b.html | 同协议、同端口、主域相同、子域不同,不可通信 | 是(cookie不可访问) |
http://111.11.11.11/b.html | 同协议、同端口、域名和该域名对应的ip,不可通信 | 是 |
http://www.domain.com:8888/b.html | 同协议、同域名、不同端口,不可通信 | 是 |
同源策略限制以下几种行为:
- Cookie、LocalStorage和IndexDB无法获取;
- DOM和Js对象无法获取;
- AJAX请求不能发送
3. 跨域问题解决方案
3.1 通过document.domain + iframe实现跨域
方案应用场景:主域相同、子域不同的跨域问题【浏览器同源策略限制:浏览器中不同域的框架之间不能进行js的交互操作】。
实现原理:两个页面都通过js强制设置document.domain为记基础主域,从而实现同域。
浏览器中不同框架(父子或兄弟)之间可以获取彼此的window对象,但却不能使用获取到的window对象和方法(例外:html5中的postMessage方法可以实现;有些浏览器如ie6也可以使用top、parent等少数几个属性),但基本获取到的是一个无用的window对象。例如有一个主页面http://XXX.domain.com/a.html, 其中嵌入一个iframe子页面:http://domain.com/b.html,明显该父页面与其中的iframe框架不同域,因此无法在主页面中通过js代码获取iframe框架中的数据:
<iframe id="iframe" src="http://domain.com/b.html" onload="onLoad()"></iframe>
<script type='text/javascript'>
function onLoad(){
let iframe = document.getElementById('iframe');
let win = iframe.contentWindow; //可获取到iframe框架的window对象,但该window对象的属性和方法基本不可用
console.log('win.document', win.document); //获取不到iframe框架的document对象
}
</script>
此时把主页面与其内部的iframe框架的document.domain设置为相同的域名,就可以获取和操作iframe中的各种对象和属性。但该方法需要注意:只能把document.domain设置成自身或者其更高一级的父域名,且主域必须相同。例如:a.b.example.com 中某个文档的document.domain 可以设成a.b.example.com、b.example.com 、example.com中的任意一个,但是不可以设成 c.a.b.example.com,因为这是当前域的子域,也不可以设成baidu.com,因为主域已经不相同了。
具体实现:
/* 父页面 */
<iframe id="iframe" src="http://domain.com/b.html" onload="onLoad()"></iframe>
<script type="text/script">
document.domain = 'domain.com'; //设置成主域
var user = 'admin';
function onLoad(){
console.log('document.getElementById("iframe").contentWindow : ', document.getElementById('iframe').contentWindow)
}
</script>
/* iframe子页面 */
<script type="text/script">
document.domain = 'domain.com'; //iframe子页面也设置成主域,与父页面相同,即使其本身为主域,依然要显示设置document.domain
console.log('获取父窗口中的变量user:', window.parent.user);
</script>
此时就可以通过js实现父页面与iframe框架之间的交互。
不过如果想在a.html页面中通过ajax直接请求b.html页面,即使设置了相同的document.domain也不可以,因此修改document.domain的方法只适用于不同子域的框架间的交互。但如果想通过ajax的方法去与不同子域的页面交互,除了使用jsonp的方法外,还可以用一个隐藏的iframe来做一个代理。原理就是让这个iframe载入一个与想要通过ajax获取数据的目标页面处在相同的域的页面,所以这个iframe中的页面是可以正常使用ajax去获取想要的数据的,然后再利用修改document.domain的方法,通过js完全控制这个iframe,让iframe去发送ajax请求,获取收到的数据。
3.2 通过location.hash + iframe实现跨域
方案应用场景:不同页面之间跨域通信
实现原理:页面a与页面b通过页面c实现相互通信,其中页面a与页面c同域,页面a与页面b不同域。三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接通过js通信。
具体实现:域A:页面a -> 通过hash值单向通信 -> 域B:页面b -> 通过hash值单向通信 -> 域A:页面c (与页面a同域,可通过parent.parent 访问页面a中的所有属性和方法)
示例:
/*页面a:http://XXX.domainA.com/a.html*/
<iframe id="iframe" src="http://XXX.domainB.com/b.html" style="display: none;"></iframe>
<script type="text/javascript">
let iframe = document.getElementById('iframe');
//向页面b传hash值
setTimeout(function(){
iframe.src = iframe.src + '#user=admin';
}, 1000);
//页面a定义方法,供页面c调用
function onCallback(data) {
console.log('来自页面c的数据:', data);
}
</script>
/*页面b:http://XXX.domainB.com/b.html*/
<iframe id="iframe" src="http://XXX.domainA.com/c.html" style="display: none;"></iframe>
<script type="text/javascript">
let iframe = document.getElementById('iframe');
//监听页面a传来的hash值,再向页面c传值
window.onhashchange = function(){
iframe.src = iframe.src + location.hash;
}
</script>
/*页面c:http://XXX.domainA.com/c.html*/
<script type="text/javascript">
//监听页面b传来的hash值
window.onhashchange = function(){
//调用同域的页面a的方法并返回数据
let data = location.hash.replace('#user=', '');
window.parent.parent.onCallback(`hello: ${data}`);
}
</script>
3.3 通过window.name + iframe实现跨域
方案应用场景:不同页面之间跨域通信。
实现原理:window的name属性具有一个特征,即 在一个窗口(window对象)的生命周期内,窗口载入的所有页面都共享一个window.name,每个页面对window.name都有读写的权限,window.name会持久存在于一个窗口载入过的所有页面中,不会因为新页面的载入而发生改变,除非在新载入的页面中对window.name重新赋值,且无论初始页面和后来载入的新页面是否处于同一个域内,都不影响window.name的这一特性,此特性也是window.name解决跨域问题的原理。可以利用window.name在不同页面之间进行通信,但其值只能是字符串形式,存储容量大概为2M左右,在不同浏览器的存储容量会有相应变化。
具体实现:利用iframe的src属性切换加载外域和本地域页面,实现跨域通信。
/*页面a:http://XXX.domainA.com/a.html*/
<script type="text/javascript">
//跨域请求数据
function proxy(url, callback){
let state = 0; //标识当前iframe加载页面为页面a的不同域页面b
let iframe = document.createElement('iframe'); //若不想在页面展示出页面跳转的处理,可将创建的iframe设置为隐藏属性
//加载跨域页面
iframe.src = url;
iframe.onload = function(){
if(state == 0){
//当前iframe框架加载页面为页面b,加载完成后,重定向加载页面c
iframe.contentWindow.location = 'http://XXX.domainA.com/c.html'; //加载跨域页面成功后,iframe框架切换页面到页面a的同域页面c,以读取window.name
state = 1; //标识当前iframe加载页面为页面a的同域页面c
}
else if(state == 1){
//当前iframe框架加载页面为页面c,加载完成后,读取同域页面c的window.name
callback(iframe.contentWindow.name);
destoryFrame();//读取完成后,销毁当前为跨域获取数据而动态加载的iframe,释放内存且保证安全性,防止被其他域访问
}
}
documen.body.appendChild(iframe);
//销毁iframe
function destoryFrame(){
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
}
//跨域请求页面b数据
proxy('http://XXX.domainB.com/b.html', function(data){
console.log('来自页面b的数据:', data);
})
</script>
/*页面b:http://XXX.domainB.com/b.html*/
<script type="text/javascript">
//设置window.name的值,可向页面a跨域传递数据
window.name = '本消息来自页面b,请查收~';
</script>
/*页面c:http://XXX.domainA.com/c.html*/
<script type="text/javascript">
//起代理页面的作用,继承来自页面b的window.name的值,并可将其传递给页面a,无需要可设置空白页
</script>
3.4 通过H5的postMessage实现跨域
方案应用场景:跨文本文档、多窗口、跨域消息传递,多用于窗口间数据通信。
实现原理:postMessage是H5引入的API,postMessage()方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档、多窗口、跨域消息传递,多用于窗口间数据通信,这也是其可以解决跨域问题的原因。postMessage在绝大多数浏览器中支持度良好,只在IE浏览器中支持度较低。
具体实现:
postMessage()方法语法:
targetWindow.postMessage(message, targetOrigin, [transfer]);
说明:
1)targetWindow:对将接受消息的窗口的引用。
获得此类引用的方法包括:
- window.open:生成一个新的窗口然后引用它;
- wendow.opener:引用创建当前窗口的父窗口;
- HTMLFrameElement.contentWindow:指定的<iframe>子页面的window对象;
- window.parent:在<iframe>子页面里引用该<iframe>所在的父页面;
- window.frames[索引值]:引用 对应索引值的<iframe>;
2)message:要发送到其他窗口的消息。
该方法使用结构化克隆算法序列化数据,因此可以将各种各样的数据对象安全地传递到目标窗口,而不需要自己序列化。
3)targetOrigin:指定要调度的事件的targetWindow的原点【也就是要接受消息的目标窗口】,可以是文字字符串"*",表示没有首选项(即目标窗口,任何页面都可接受到当前发送的消息),也可以是URI(即指定唯一能接受此消息的目标窗口)。如果在计划调度事件时,targetWindow的协议、主机、端口与targetOrigin的不匹配,则不会调用postMessage方法,只有两者的三个条件都相同时才会调度该事件。该机制可以控制发送消息的位置;例如,如果postMessage()
用于传输密码,则该参数必须是URI,其来源与包含密码的消息的预期接收者相同,以防止恶意第三方拦截密码。始终提供具体的targetOrigin
,而不是*
,如果您知道其他窗口的文档应该位于何处。未能提供特定目标会泄露您发送给任何感兴趣的恶意站点的数据。
4)transfer[可选]:是与消息一起传输的Transferable对象序列,这些对象的所有权将提供给目标端,并且它们在发送端不可再用。
示例:
父页面a.html内嵌入不同域子页面b.html,需要接收信息则为 window 添加 "message" 事件监听。
<!-- 页面a:http://XXX.domainA.com/a.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>iframe跨域</title>
</head>
<body>
<div class="header">
<h3>主页面</h3>
</div>
<iframe id="iframe" class="iframe" onload="onLoad()" src="http://XXX.domainB.com/b.html"></iframe>
</body>
</html>
<script type="text/javascript">
function onLoad(){
//向子页面b.html发送消息
let data = {
msg: '来自父页面a.html的消息'
}
window.iframe[0].postMessage(JSON.stringify(data), 'http://XXX.domainB.com');
}
//接收来自b.html的信息
window.addEventListener('message', function(event){
console.log('a.html接收到的信息:', event.data);
});
</script>
<!-- 页面b:http://XXX.domainB.com/b.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>iframe跨域</title>
</head>
<body>
<div class="header">
<h3>子页面</h3>
</div>
</body>
</html>
<script type="text/javascript">
//接收来自父页面a.html的信息
window.addEventListener('message', function(event){
console.log('b.html接收到的信息:', event.data);
let returnData = JSON.parse(event.data);
returnData.msg = '来自b.html的信息';
//向父页面a.html发送消息
window.parent.postMessage(JSON.stringify(returnData), 'http://XXX.domainA.com/a.html');
});
</script>
接收到的消息对象event的属性说明:
- data:从另一个窗口传递的对象,即postMessage()的 message;
- origin:调用当时发送消息的窗口的原点
postMessage
。此字符串是协议和“://”的串联,如果存在,则为主机名,如果存在端口,则“:”后跟端口号,并且与给定协议的默认端口不同。请注意,此来源不保证是该窗口的当前或未来来源,该窗口可能已被导航到调用postMessage
后的其他位置。 - source:对发送消息的window对象的引用,可以使用它来建立两个不同源的窗口之间的双向通信。event.source.postMessage("postMessage", event.origin);
3.5 通过JSONP实现跨域
方案应用场景:跨域HTTP请求问题【浏览器同源策略限制:不能通过ajax的方法去请求不同源中的文档】。
实现原理:动态添加一个请求带参网址的<script>标签,通过html的script标记进行跨域请求,并在响应中返回要执行的script代码,使JSONP协议替代ajax XmlHttpRequest协议,实现跨域通信请求。
通常为了减轻web服务器的负载,会将js、css、img等静态资源分离到另一台独立域名的服务器上,浏览器允许在html 页面中再过相应的标签从不同域名下加载静态资源。在js中,直接用XMLHttpRequest请求不同域上的数据是不被允许的,但允许在页面上引入不同域上的js脚本。基于此原理,可以通过动态创建script标签,再请求一个带回调函数参数的网址实现跨域通信,即JSONP解决跨域问题的原理。
JSONP的最基本的原理是:动态添加一个<script>标签,而script标签的src属性是没有跨域的限制的。这样说来,这种跨域方式其实与ajax XmlHttpRequest协议无关了。
如果设为dataType: 'jsonp',这个$.ajax方法就和ajax XmlHttpRequest没什么关系了,取而代之的则是JSONP协议。JSONP是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问。
JSONP即JSON with Padding。由于同源策略的限制,XmlHttpRequest只允许请求当前源(域名、协议、端口)的资源。如果要进行跨域请求, 我们可以通过使用html的script标记来进行跨域请求,并在响应中返回要执行的script代码,其中可以直接使用JSON传递javascript对象。 这种跨域的通讯方式称为JSONP。
具体实现:
(1)原升js实现:
<script type='text/javascript'>
let script = document.createElement('script');
script.type = 'text/javascript';
//传参并指定回调执行函数onCallback
script.src = 'http://XXX.com:8080/login?callback=onCallback';
document.head.appendChild(script);
//回调执行函数(服务器端返回时即执行)
function onCallback(result){
console.log(JSON.stringify(result));
}
</script>
(2) jquery ajax
$.ajax({
url: 'http://XXX.com:8080/login';
type: 'get'; //请求方式[jsonp只能实现get一种请求]
dataType: 'jsonp'; //预期服务器返回的数据类型为jsonp
jsonpCallback: 'onCallback'; // 为 jsonp 请求指定一个回调函数名
data: {}; //发送到服务器的数据, 将自动转换为请求字符串格式
});
JSONP缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。JSONP是一种脚本注入(Script Injection)行为,所以有一定的安全隐患。
3.6 CORS(跨域资源共享)
方案应用场景:跨域HTTP请求问题【浏览器同源策略限制:不能通过ajax的方法去请求不同源中的文档】。
实现原理:
CORS是一个W3C标准,全称是“跨域资源共享(Cross-origin resource sharing)”。它允许浏览器向跨源的(协议+域名+端口)服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。它的通信都由浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信是没有差别的。浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将CORS请求分为两类:简单请求(simple request) 和 非简单请求(no-so-simple request)。
浏览器发出CORS简单请求,只需要在头信息中增加一个Origin字段。
浏览器发出CORS非简单请求,会在正式通信之前增加一次HTTP查询请求,成为“预检”请求(preflight)。浏览器先询问服务器,当前页面所在的域名是否在服务器的许可白名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则会报错。
构成简单请求的条件:
(1)请求方式为 HEAD、GET、POST 三者之一;
(2)HTTP的头信息只限于以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain);
具体实现:跨域的简单demo集合(引用demo)
3.7 代理proxy跨域
因为JS同源策略是浏览器的安全策略,所以在浏览器客户端不能跨域访问,而服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就没有跨越问题。简单地说,就是浏览器不能跨域,后台服务器可以跨域。
3.7.1 nginx代理跨域
1)浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入配置:
location / {
add_header Access-Control-Allow-Origin *;
}
2)通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
3.7.2 Nodejs中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
3.8 WebSocket协议跨域
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
原生WebSocket API使用起来不太方便,可以使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
引用链接:
- https://segmentfault.com/a/1190000011145364
- https://segmentfault.com/a/1190000015597029
- https://blog.csdn.net/z69183787/article/details/19191385
- https://www.cnblogs.com/2050/p/3191744.html
- https://www.w3cschool.cn/fetch_api/fetch_api-lx142x8t.html
- https://juejin.im/post/5b8359f351882542ba1dcc31
- https://www.w3cschool.cn/fetch_api/fetch_api-lx142x8t.html
- http://www.cnblogs.com/cityspace/p/6858969.html
- https://github.com/FatDong1/cross-domain
- https://www.toutiao.com/i6620929432188092932/