跨域及跨域方法小结

记:在近期的面试问题中,做前端,跨域问题必问,跨域涉及到数据的获取和交互,在前端来说尤为重要。而在此之前,每次被问到跨域,只能一知半解的回答只知道jsonp,然而对于jsonp的原理还不是很清晰。以下是对跨域知识的全面梳理,在未涉足的方法,先进行提点一下,后续继续补充和完善。
一、跨域的原因

这是由于浏览器的同源策略,所谓的同源策略就是域名、端口号、协议三者要完全相同,才能被界定为同源。只要三者有一个不同,如果要进行通信,就会产生跨域问题。
注意点:对于协议和端口造成的跨域问题,“前端”无能为力
域仅仅是同通过“URL首部”来识别,而不会去尝试判断相同的ip地址对应的两个域是否
在同一个ip上,“URL首部”指window.location.protocol+window.location.host

二、跨域的方法

常见的方法有jsonp、window.domain+iframe、window.name+iframe、location.hash+iframe、
postMessage()+iframe、cors跨域、WebSocket协议跨域、nginx代理跨域等方法,下面一一介 绍他们的实现原理以及适应范围和局限性

  1. jsonp跨域
全称是JSON with Padding,是使用Ajax请求实现不同源的跨域。 其基本原理:网页通过添加一个  <script>  元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
jsonp有两个参数,JSON数据和回调函数,<script>标签的src属性指定一个网址,传递一个callback参数,该参数值是一个定义在客户端的回调函数名。在服务器端请求到的文件会直接作为代码运行,转换为JSON数据,该JSON数据会作为参数,传入客户端的回调函数,客户端通过回调函数可以处理服务器响应的数据。
举个栗子:
当前页面为 http://a.com/a.html ,需要请求 http://b.com/test.js
在a.html定义如下:
<script type="javascript">
//定义回调函数
function callback(data){
alert(data.meaasge);
}
</script>
<script type="javascript" src="http://b.com/test.js?callback=callback"></script>
以上就完成了jsonp的跨域请求。

也可以通过jquery封装好的$.ajax()方法来进行jsonp的请求,请求格式如下:
$.ajax({
type:'GET',
dataType:'jsonp',
jsonpCallback:'callback',
data:{
传入的其他参数
}
});

原生动态的插入script标签的实现:
let script = document.createElement("script");
script.src =' http://b.com/test.js ?callback=callback';
document.body.appendChild(script);
function callback(data){
console.log(data);
}
由于script标签,只能用于get请求,也就是说jsonp只能处理get请求的跨域,适用在单向跨域,用来请求数据,不能解决两个不同域的页面进行相互通信的问题。

2.document.domain+iframe跨域的实现原理
采用这种方式跨域的要求是主域名必须相同,也就是说除了最底层域名不一样其他都相同,例如a.hxc.com、b.hxc.com、c.hxc.com三者的主域名都是hxc.com,因此可以采用此方式实现跨域。
注意:document.domain设置成自身或者是更高一级的父域
举个栗子:
前提假设:a.hxc.com/a.html与b.hxc.com/b.html对应两个不同的ip服务器

在a.html设置如下:
<iframe id="iframe" src="http://b.hxc.com/b.html" οnlοad="test()"></iframe>
<script type="javascript">
document.domain = 'hxc.com'; //设置主域名
function test(){
alert(document.getElementById('iframe').contentWindow);
}
</script>
虽然在a.html已经设置好了,但是这样是不会成功的。因为b.html的主域名还不一样,因此需要在b.html进行设置:
<script type="javascript">
document.domain = 'hxc.com' ; // 设置主域名
</script>
这样一个跨域的请求就实现了。

3.window.name+iframe实现跨域原理
这里主要使用的是window对象的name属性,该属性的特征是:在一个window的生命周期里,窗口载入的所有页面都共享一个window.name,每个页面都有读写window.name的权限,window.name会存在于一个窗口中载入的所有页面中,并不会因为新页面的载入而进行重置。
利用以上的特性,我们可以在某个页面先将数据存在window.name里,然后在进行页面跳转后,去访问和读取window.name属性值,即可达到跨域的目的。由于安全原因,浏览器会保证window.name为string类型。

举个栗子:
首先在被请求的文件里设置好window.name的值,被请求的页面为
然后在发起请求的http://b.hxc.com/index,html进行相关的设置,利用iframe的标签的跨域能力,加载了请求的页面,但是会因为不同源的原因,而无法操作该页面的任何属性,因此在index.html的同级目录下创建一个空白的代理页面proxy.html,通过重置src属性值,这样就做到了同源。为了同源的其他方法还有,“about:blank javascript: data: ”这里面的内容都继承载入他们页面的源。
<script type="javascript">

let iframe = document.createElement('iframe');
iframe.style.display = 'none';
var state = 0;

iframe.onload = function(){
if(state === 1){
var data = JSON.parse(iframe.contentWindow.name);
console.log(data);
iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe);
}else if(state === 0){
state = 1;
iframe.contentWindow.location= 'http://b.hxc.com/proxy.html';
}
}
iframe.src = " http://a.hxc.com/data.php ";
document.body.appendChild(iframe);
</script>
这里首先iframe要具备跨域的能力,其次window.name属性值在页面刷新仍然存在。局限性是只能用于get的请求。

4.location.hash+iframe跨域实现原理
因为父窗口可以对iframe进行URL读写,iframe也可以读写父窗口的URL,URL有一部分被称为hash,就是#号及其后面的字符,它一般用于浏览器锚点定位,Server端并不关心这部分,应该说HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变URL的hash部分来进行双向通信。每个window通过改变其他 window的location来发送消息(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe),并通过监听自己的URL的变化来接收消息。这个方式的通信会造成一些不必要的浏览器历史记录,而且有些浏览器不支持onhashchange事件,需要轮询来获知URL的改变,最后,这样做也存在缺点,诸如数据直接暴露在了url中,数据容量和类型都有限等。
当baidu.com/a.html传数据到goole.com/b.html:
在a.html设置:
将要传递的数据放在data里
<iframe src="goole.com/b.html#data" ></iframe>
在b.html监听url的hash值的变化,采用的方法是h5的hashchange()方法
如果hash值有变化,则获取该值
var newHash = parent.location.hash.substring(1);

当b.html传递数据到a.html:
由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe
在b.html创建一个隐藏的iframe,其src属性指向'baidu.com/proxy.html#data',并挂上要传递的hash值。
proxy.html监听url的hash的变化,然后修改a.html的url,因为此时两页面是同源的。
a.html监听到url变化,则取出相应的hash值。
b.html的相关代码:
try{
parent.location.hash = 'data';
}catsh(e){
//针对ie和chrom不能直接修改parent.location.hash
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = "http://baidu.com/proxy.html#data";
document.body.appendChild(ifrproxy);
}
proxy.html页面的相关代码:
//因为parent.parent(即baidu.com/a.html)和baidu.com/proxy.html属于同一个域,所
以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
采用该方法可以实现两个页面的互相通信,但是只能用于get请求。与window.name的方法一样,利用的都是全局的属性。

5.postMessage()跨域实现

postMessage()是h5实现的一个API,该方法被调用后,会在所有页面脚本执行完毕之后向目标窗口派发一个   MessageEvent 消息。
MessageEvent消息有四个属性需要注意:
message 属性表示该message 的类型; 
data 属性为 window.postMessage 的第一个参数;
origin 属性表示调用window.postMessage() 方法时调用页面的当前状态; 
source 属性记录调用 window.postMessage() 方法的窗口信息
语法:
otherWindow.postMessage(message,targetOrigin,[transfer])
otherWindow : 是其他窗口的一个引用,比如iframe的contentWindow属性,执行window.open返回的窗口对象,或者是命名过的数值索引的window.iframes.
meaasge : 将要发送到其他window的数据,它将会被结构化克隆算法序列化
targetOrigin : 通过该属性来指定哪个窗口可以接收信息,如果不指定可以值为“*”或者一个URI.利用这个机制,可以限定接收消息的窗口,如果目标窗口不符合同源策略,则消息就不会被发送。最好指定明确的值,这样可以防止第三方恶意截获数据
transfer (该属性可选):是一串和message 同时传递的  Transferable  对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
其他window可以监听传过来的message:
eg:
window.addEventListener("message",receiveMessage,false);
function receiveMessage(event){
var origin = event.origin || event.originalEvent.origin; if (origin !== "http://example.org:8080") return;
}

message的属性如下:
data:从其他 window 中传递过来的对象。
origin:调用 postMessage  时消息发送方窗口的  origin  . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。请注意,这个origin不能保证是该窗口的当前或未来origin,因为postMessage被调用后可能被导航到不同的位置。
source:对发送消息的 窗口 对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信。
这里涉及到的安全问题,如果不希望从其他网站接受到message,不要为message设置监听事件。如果要从其他网站接收message时,要始终验证origin和sourse属性值,确保发件人身份
以下是实现的栗子:
a.html的相关代码: 域名为 http://a.hxc.com/a.html
<iframe id='iframe' src='http://b.hxc.com/b.html' style="display:none"></iframe>
<script type="javascript">
var iframe = document.getElementById('iframe');
iframe.onload = function(){
var data = {name:'arm'};
//向b.html传送数据
iframe.contentWindow.postMessage(JSON.stringify(data),
'http://b.hxc.com/b.html');
};
//监听b.html页面传过来的数据
window.addEventListener('message',function(event){
//验证发件人的身份
if(event.origin =='http://b.hxc.com/b.html' &&
event.sourse == 'window.parent'){
alert('data from b:'+event.data);
}
})
</script>
b.html的相关代码: http://b.hxc.com/b.html
<script type="javascript">
//接收来自a页面的消息
window.addEventListener('message',function(event){
//验证发件人身份
if(event.origin =='http://a.hxc.com/a.html' &&
event.sourse == 'iframe.contentWindow'){
alert('data from b:'+event.data);
};

//发送数据
var data = JSON.parse(event.data);
if(data){
data.number = 16;
//处理后返回a页面
window.parent.postMessage(JSON.stringify(data),
'http://a.hxc.com/a.html');
}
},false);
</ script>

6.cors跨域资源共享

允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了Ajax只能同源使用的限制。需要浏览器和服务器同时支持,实现的关键在服务器,只要服务器实现cors接口,就可以跨源通信。目前IE9以下的需要额外靠XDomainRequest对象来支持cors。
cors有两种请求,分为简单请求和非简单请求,浏览器对这两种的请求的处理方式是一样的

简单请求:
  • 该请求有一下局限:
  • 请求方式为:HEAD POST GET
  • http头信息不超过一下字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(限于三个值:appication/x-www-form-urlencoded、multipart/form-data、text/plain)
浏览器直接发送cors请求,在头信息会添加一个Origin字段,如下:
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0
...
Origin字段说明来自哪个源,即协议、端口和域名,服务器根据这个值,决定是否同意这次请求。
请求失败:如果Origin值不在服务器的许可范围内,服务器还是会返回一个正常的HTTP回应,只不过回应头信息里没有Access-Control-Allow-Origin这个字段,并且错误会被XMLHttpRequest对象的onerror回调函数捕获。这种错误通过HTTP状态码无法识别,因为此时状态码很可能是200.
请求成功:响应头部里会多出几个与cors相关的头信息字段:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html;
charset=utf-8
其中Access-Control开头的字段解析:
Access-Control-Allow-Origin:必须,该值是发送cors请求是的Origin值,也可以是“*”,表示接受任何域名请求
Access-Control-Allow-Credential:可选,为布尔值,表示是否允许发送cookie,默认情况下为false,cookie不包含在cors请求中。当设置为true时,表示服务器允许cookie包含在cors请求中。这个值使用时,只能设置为true,否则不需要删除即可。
Access-Control-Expose-Headers:可选,cors请求时,XMLHttpRequest对象只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
如果想拿到其他字段,则必须在该字段里指定。
在发送cors请求时,默认不发送cookie和HTTP认证信息,如果需要把cookie发送到服务器,一方面需要指定Access-Control-Allow-Credential设置为true
另一方面。开发者在需要在ajax请求中打开withCredential属性。设置如下:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText); } };
// jquery $.ajax({
...    
xhrFields: {        withCredentials: true // 前端设置是否带cookie    },  
 crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
... });
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。 但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。


非简单请求:
条件为:请求方式为PUT或DELETE,或者Content-Type类型的值为application/json
非简单请求在正式cors请求时,会增加一次HTTP查询请求,称为预检请求(preflighted request)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
除了Origin字段,"预检"请求的头信息包括两个特殊字段。
  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
预检请求的回应
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html;
charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。
服务器回应的其他CORS相关字段如下:
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
Access-Control-Allow-Credentials: 该字段与简单请求时的含义相同。
Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器正常请求回应:
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
浏览器的正常CORS请求。上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin字段是每次回应都必定包含的
cors跨域可以处理所有的HTTP方式请求。
7.WebSocket协议的跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端的代码实现:
<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script> var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg); });
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.'); }); });
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value); };
</script>
node server代码:

相关的还有 node实现跨域nginx反向代理实现跨域,但是我目前还没看到那么多,等我后续学习完,继续补上。



参考了掘金论坛大佬写的,可以去看大佬文章:https://juejin.im/post/5a2f92c65188253e2470f16d#heading-3



























  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值