前端跨域知识总结

  • 2018-01-12
  • 0
  • 0

2018-01-18 在 Dynamsoft 市场部例会上所做分享的部分内容

一、 什么是跨域?

跨域从字面意思看,就是跨域名,但实际上跨域的范围绝对不止那么狭隘。具体概念如下:只要协议、域名、端口有任何一个不同,都被当作是不同的域。之所以会产生跨域这个问题呢,其实也很容易想明白,要是随便引用外部文件,不同域名、不同 ip 下的页面能随意引用彼此的文件的话,浏览器是很容易懵逼的,安全也得不到保障了。但在安全限制的同时也给注入 iframe 或是 ajax 应用上带来了不少麻烦。所以我们要通过一些方法使本域的 js 能够操作其他域的页面对象或者使其他域的 js 能操作本域的页面对象(iframe 之间)。下面是具体的跨域情况详解:

URL 说明 是否允许通信
http://www.a.com/a.js 同一域名下的文件 允许
http://www.a.com/b.js
http://www.a.com/lab/a.js 同一域名下不同文件夹 允许
http://www.a.com/script/b.js
http://www.a.com:8000/a.js 同一域名,不同端口 不允许
http://www.a.com/b.js
http://www.a.com/a.js 同一域名,不同协议 不允许
https://www.a.com/b.js
http://www.a.com/a.js 域名和域名对应ip 不允许
http://70.32.92.74/b.js
http://www.a.com/a.js 主域相同,子域不同 不允许(cookie这种情况下也不允许访问)
http://script.a.com/b.js
http://www.a.com/a.js 同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)
http://a.com/b.js
http://www.cnblogs.com/a.js 不同域名 不允许
http://www.a.com/b.js

二、通过 document.domain 跨域

前面说过了,浏览器有一个同源策略,其限制之一是不能通过 ajax 的方法去请求不同源中的文档。 第二个限制是浏览器中不同域的框架之间是不能进行 js 的交互操作的。不同的框架之间是可以获取 window 对象的,但却无法获取相应的属性和方法。

比如,有一个页面,它的地址是 http://localhost//kuayu/domain.html ,在这个页面里面有一个 iframe,它的 src 是 http://localhost/:86/kuayu/domain.html, 很显然,这个页面与它里面的 iframe 框架是不同域的,所以我们是无法通过在页面中书写 js 代码来获取 iframe 中的东西的:

<script type="text/javascript">
    function test(){
        var iframe = document.getElementById('ifame');
        var win = document.contentWindow; //可以获取到 iframe 里的 window 对象,但该 window 对象的属性和方法几乎是不可用的
        var doc = win.document; //这里获取不到 iframe 里的 document 对象
        var name = win.name; //这里同样获取不到 window 对象的 name 属性
    }
</script>
<iframe id = "iframe" src="http://localhost/:86/kuayu/domain.html" onload = "test()"></iframe>

上述代码段里只能获取到 iframe 的 windows 对象,但是 windows 对象的方法和属性却大多不可用。

这个时候,document.domain 就可以派上用场了,我们只要把 parent 页面和 child 页面的 document.domain 都设成相同的域名就可以了:

http://localhost//kuayu/domain.html 页面设置 document.domain:

<div id="c">我是 parent 页面里的内容</div>
<div id="b">点击这里获取 iframe 页面的内容</div>
<iframe id="iframe" src="http://localhost/:86/kuayu/domain.html"></iframe>
<script type="text/javascript">
	document.domain = '192.168.8.89';

	var iframe = document.getElementById('iframe');
	var win = iframe.contentWindow;
	document.getElementById('b').onclick = function(){
		document.getElementById('b').innerHTML = win.document.getElementById('a').innerHTML
	}
</script>

http://localhost/:86/kuayu/domain.html 也设置 document.domain:

<div id="a">我是 iframe 页面里的内容</div>
<div id="d">点击这里获取 parent 的内容</div>
<script type="text/javascript">
    document.domain = '192.168.8.89';

    document.getElementById('d').onclick = function(){
	document.getElementById('d').innerHTML = window.parent.document.getElementById('c').innerHTML;
    }
</script>
注意: document.domain 的设置是有限制的,我们只能把 document.domain 设置成自身或更高一级的父域,且主域必须相同,即修改 document.domain 的方法只适用于不同子域或不同端口号的框架之间的交互。

三、通过 location.hash 跨域

因为父窗口可以对 iframe 进行 URL 读写,iframe 也可以读写父窗口的 URL,URL 有一部分被称为 hash,就是 # 号及其后面的字符,它一般用于浏览器锚点定位,Server 端并不关心这部分,应该说 HTTP 请求过程中不会携带hash,所以这部分的修改不会产生 HTTP 请求,但是会产生浏览器历史记录。此方法的原理就是改变 URL 的 hash 部分来进行双向通信。每个 window 通过改变其他 window 的 location 来发送消息,并通过监听自己的 URL 的变化来接收消息。

这个方式的通信会造成一些不必要的浏览器历史记录,而且有些浏览器不支持 onhashchange 事件,需要轮询来获知 URL 的改变,最后,这样做也存在缺点,诸如数据直接暴露在了 url 中,数据容量和类型都有限等。下面举例说明:

父页面 http://localhost//kuayu/hash.html:

<ul>
    <li>Click to send the first message</li>
    <li>Click to send the second message</li>
    <li>Click to send the thrid message</li>
</ul>
<iframe id="myIFrame" src="http://alvinwp.com/demo/kuayu/hash.html"></iframe>
<div id="b">接受 iframe 页面发送的消息</div>
<script>
//点击发送消息
$("li").each(function(index, element) {
	var _this = $(this);
    _this.click(function(){
		document.getElementById('myIFrame').src = 'http://alvinwp.com/demo/kuayu/hash.html' + '#' + _this.html();
	});
});

//监听 hash 变化
window.onhashchange = checkMessage;
function checkMessage() {
  var message = window.location.hash;
  $("#b").html(message);
}
</script>

iframe 页面 http://alvinwp.com/demo/kuayu/hash.html:

<div id="a">接受 parent 页面发送的消息</div>
<script>
//监听 hash 变化
window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  document.getElementById('a').innerHTML = message;
  
  switch(message)
	{
	case '#Click to send the first message':
	  parent.location.href = 'http://localhost//kuayu/hash.html#received-first';
	  break;
	case '#Click to send the second message':
	  parent.location.href = 'http://localhost//kuayu/hash.html#received-second';
	  break;
	case '#Click to send the thrid message':
	  parent.location.href = 'http://localhost//kuayu/hash.html#received-thrid';
	  break;
	default:
	  parent.location.href = 'http://localhost//kuayu/hash.html#received';
	}  
}
</script>

四、通过 HTML5 的 postMessage 方法跨域

HTML5为了解决跨域问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

高级浏览器 Internet Explorer 8+, chrome,Firefox , Opera 和 Safari 都将支持这个功能。这个功能主要包括接受信息的 “message” 事件和发送消息的 “postMessage” 方法。

比如 http://localhost//training/kuayu/postMessage.html 页面向 iframe 里的 http://alvinwp.com/demo/kuayu/postMessage.html 页面发送一句 “hello world!”,iframe 页面再向 parent 页面发送一句 “我收到信息啦!”,代码如下:

父页面 http://localhost//kuayu/postMessage.html :

<div id="a">我是主页面</div>
<div id="c">iframe 内嵌页面返回的信息会显示在这里</div>
<iframe id="childPage" src="http://alvinwp.com/demo/kuayu/postMessage.html"></iframe>
<br/>
<input id="btnSubmit" onclick="sendData()" type="button" value="给 iframe 页面发送文本:hello world!"/>
<script>
//发消息
function sendData(){

    var ifr = document.getElementById('childPage');  

    var targetOrigin = "http://alvinwp.com";  

    ifr.contentWindow.postMessage('hello world!', targetOrigin);  

};

//收消息
var onmessage = function (event) {  
  var data = event.data;//消息内容  
  var origin = event.origin;//消息来源地址  
  var source = event.source;//源 Window 对象
  if(origin=="http://alvinwp.com"){  
	document.getElementById("c").innerHTML = data;
  }  

};  

if (typeof window.addEventListener != 'undefined') { 

  window.addEventListener('message', onmessage, false);  
  
} else if (typeof window.attachEvent != 'undefined') { 
 
  //for ie 
  window.attachEvent('onmessage', onmessage);  
  
}  
</script>

postMessage 的使用方法:otherWindow.postMessage(message, targetOrigin);

  1. otherWindow: 指目标窗口,也就是给哪个 window 发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口
  2. message: 是要发送的消息,类型为 String、Object (IE8、9 不支持)
  3. targetOrigin: 是限定消息接收范围,不限制请使用 “*”

iframe 页面 http://alvinwp.com/demo/kuayu/postMessage.html:

<div>我是 iframe 内嵌页面</div>
<div id="b">主页面发过来的信息会显示在这里</div>
<script>
var onmessage = function (event) {  
  var data = event.data;//消息  
  var origin = event.origin;//消息来源地址  
  var source = event.source;//源 Window 对象  
  if(origin=="http://localhost/"){  
	document.getElementById("b").innerHTML = data;
	
	//发消息 
    source.postMessage('我收到信息啦!', origin);  
  }  

};  

if (typeof window.addEventListener != 'undefined') {  

  window.addEventListener('message', onmessage, false);  

} else if (typeof window.attachEvent != 'undefined') {  

  //for ie  
  window.attachEvent('onmessage', onmessage);  

}  
</script>

五、通过 window.name 跨域

window 对象有个 name 属性,该属性有个特征:即在一个窗口 (window) 的生命周期内,窗口载入的所有的页面都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

比如:我们某个窗口的页面 http://localhost//kuayu/name.html 中输入下面的代码:

<a class="link1" href="http://alvinwp.com/demo/kuayu/name.html">http://alvinwp.com/demo/kuayu/name.html</a><br/>

<a class="link2" target="_blank" href="http://alvinwp.com/demo/kuayu/name.html">http://alvinwp.com/demo/kuayu/name.html</a>

<script>
window.name = "Information from the parent window";
</script>

http://alvinwp.com/demo/kuayu/name.html:

<script>
alert("window.name is: "+ window.name);
</script>

可以看到,点击 link1 在当前窗口打开页面,弹出的 window.name 为之前设置过的 “Information from the parent window”;而点击 link2 会在新窗口打开页面,弹出的 window.name 为空。基于这个思想,我们可以在某个页面设置好 window.name 的值,然后跳转到另外一个页面。在这个页面中就可以获取到我们刚刚设置的 window.name 了。

由于安全原因,浏览器始终会保持 window.name 是string 类型。

同样这个方法也可以应用到和 iframe 的交互来:

比如:我的页面 (http://damonare.cn/index.html) 中内嵌了一个 iframe:

<iframe id="iframe" src="http://www.google.com/iframe.html"></iframe>

在 iframe.html 中设置好了 window.name 为我们要传递的字符串。

我们在 index.html 中写了下面的代码:

var iframe = document.getElementById('iframe');
var data = '';

iframe.onload = function() {
    data = iframe.contentWindow.name;
};

我们会发现报错了!报错是肯定的,因为两个页面不同源嘛,想要解决这个问题可以这样干:

var iframe = document.getElementById('iframe');
var data = '';

iframe.onload = function() {
    iframe.onload = function(){
        data = iframe.contentWindow.name;
    }
    iframe.src = 'about:blank';
};
提示:或者将里面的 about:blank 替换成某个同源页面(about:blank,javascript: 和 data: 中的内容,继承了载入他们的页面的源。)

这种方法与 document.domain 方法相比,放宽了域名后缀要相同的限制,可以从任意页面获取 string 类型的数据。

六、通过 jsonp 跨域

JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。

网页通过添加一个<script>元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 资料,而这种使用模式就是所谓的 JSONP。用 JSONP 抓到的资料并不是 JSON,而是任意的能执行的 JavaScript,用 JavaScript 直译器执行而不是用 JSON 解析器解析。

刚才说的这几种都是双向通信的,即两个 iframe,页面与 iframe 或是页面与页面之间的,下面说几种单项跨域的(一般用来获取数据),因为通过 script 标签引入的 js 是不受同源策略的限制的。所以我们可以通过 script 标签引入一个 js 或者是一个其他后缀形式(如 php,jsp 等)的文件,此文件返回一个 js 函数的调用

比如,http://localhost//training/kuayu/JSONP.html 页面需要获取一个不同域上的 json 数据,假设这个 json 数据地址是 http://alvinwp.com/demo/kuayu/jsonp.json;那么页面代码就可以如下:

<ul>
    <li>Name: <span id="a"></span></li>
    <li>Age: <span id="b"></span></li>
    <li>Weight: <span id="c"></span></li>
</ul>
<script type="text/javascript">
    function dosomething(jsondata){
    	document.getElementById('a').innerHTML = jsondata.Nmae;
	document.getElementById('b').innerHTML = jsondata.Age;
	document.getElementById('c').innerHTML = jsondata.Weight;
    }
</script> 
<script src="http://alvinwp.com/demo/kuayu/jsonp.php?callback=dosomething"></script>

我们看到获取数据的地址后面还有一个 callback 参数,按惯例是用这个参数名,但是你用其他的也一样。当然如果获取数据的 jsonp 地址页面不是你自己能控制的,就得按照提供数据的那一方的规定格式来操作了。

因为是当做一个 js 文件来引入的,所以 http://alvinwp.com/demo/kuayu/jsonp.json 返回的必须是一个能执行的 js 文件,所以这个页面的 php 代码可能是这样的(一定要和后端约定好):

<?php
$callback = $_GET['callback'];//得到回调函数名
$data = '{
  "Nmae": "Alvin",
  "Age": "xx",
  "Weight": "8.8.8.8"
}';//要返回的数据
echo $callback.'('.json_encode($data).')';//输出
?>

最终,输出结果为:dosomething([‘a’,’b’,’c’]);

或者可以把 jsonp.json 替换成内容如下的 jsonp.json 或 jsonp.jsp 文件:

dosomething(
{
  "Nmae": "Alvin",
  "Age": "xx",
  "Weight": "8.8.8.8"
}
);

JSONP 的优缺点:

  • JSONP 的优点是:它不像 XMLHttpRequest 对象实现的 Ajax 请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要 XMLHttpRequest 或 ActiveX 的支持;并且在请求完毕后可以通过调用
    callback 的方式回传结果。
  • JSONP 的缺点则是:它只支持 GET 请求而不支持 POST 等其它类型的 HTTP 请求;它只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行 JavaScript 调用的问题。

七、通过 CORS 跨域

CORS(Cross-Origin Resource Sharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。

对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

需要设置的几个 HTTP 头信息如下:

Access-Control-Allow-Origin: http://alvinwp.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header

服务器的设置可参考下述代码:

location / {
     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
     }

     if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
     }

     if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
     }
}

服务器端对于 CORS 的支持,主要就是通过设置 Access-Control-Allow-Origin 来进行的。如果浏览器检测到相应的设置,就可以允许 Ajax 进行跨域的访问。关于 CORS 更多了解可以看下阮一峰老师的这一篇文章:跨域资源共享 CORS 详解

CORS 和 JSONP 对比:

  • JSONP 只能实现 GET 请求,而 CORS 支持所有类型的 HTTP 请求。
  • 使用 CORS,开发者可以使用普通的 XMLHttpRequest 发起请求和获得数据,比起 JSONP 有更好的错误处理。
  • JSONP 主要被老的浏览器支持,它们往往不支持 CORS,而绝大多数现代浏览器都已经支持了 CORS)。

CORS 与 JSONP 相比,无疑更为先进、方便和可靠。

八、Demo 代码

针对上述每种跨域方法各写了一个简单 Demo,可下载:kuayu.zip

GitHub: crossDomain

推荐阮云峰老师一篇写的非常好的博文:浏览器同源政策及其规避方法

评论

还没有任何评论,你来说两句吧