跨域

什么是跨域

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源。

为什么要跨域

因为所有浏览器都实行同源策略。什么是同源呢?两个页面地址中的协议、域名和端口号一致,则表示同源。

而同源策略限制不同源页面的以下几种行为:

  1. Cookie、LocalStorage 和 IndexDB 无法读取
  2. DOM 和 JS 对象无法获得
  3. AJAX 请求不能发送

怎么跨域

JSONP(JSON with Padding,填充式JSON)

在JS中,我们虽然不能直接用XMLHttpRequest请求不同域上的数据,但是却可以用<script>标签在页面上引入不同域上的JS脚本文件,JSONP正是利用这个特性来实现的。

JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,而数据就是传入回调函数中的JSON数据。

<script type="text/javascript">
    function dosomething(jsondata){
        //处理获得的json数据
    }
</script>
<script src="http://example.com/data.php?callback=dosomething"></script>

首先第一个<script>标签定义了一个处理数据的函数;
第二个<script>标签载入一个JS文件,http://example.com/data.php 是数据所在地址,但是因为是当做JS来引入的,所以http://example.com/data.php返回的必须是一个能执行的js文件;
最后JS文件载入成功后会执行我们在URL参数中指定的函数,并且会把我们需要的JSON数据作为参数传入。所以php应该是这样的:

<?php
    $callback = $_GET['callback'];					//得到回调函数名
    $data = array('a','b','c');						//要返回的数据
    echo $callback.'('.json_encode($data).')';		//输出
?>

最终,输出结果为:dosomething(['a','b','c'])
从上面可以看出JSONP是需要服务器端的页面进行相应的配合的。

优点:

  1. 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;
  2. 能够直接访问响应文本,支持在浏览器与服务器之间双向通信

缺点:

  1. 只能使用Get请求
  2. 不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败
  3. JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保
CORS(Cross-Origin Resource Sharing,跨源资源共享)
  • CORS是一个W3C标准,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制,目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
  • CORS定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
  • 实现此功能非常简单,只需由服务器发送一个响应标头即可。

CORS的两种请求

浏览器将CORS分为两种请求,一种是简单请求,另外一种对应的肯定就是非简单请求。

只要同时满足下面两大条件,就属于简单请求:

请求的方法是一下的三种方法之一:

  • HEAD
  • GET
  • POST

HTTP的头信息不超过以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type: 只限于三个值:application/x-www-form-urlencodedmultipart/formdatatext/plain

凡是不同时满足以上两种条件,就属于非简单请求。

浏览器对于两种请求处理是不一样的。

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在HTTP请求报文首部,增加一个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字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

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

上面的HTTP响应报文首部信息中,有三个与CORS请求相关的字段,都是以Access-Control-开头。

Access-Control-Allow-Origin

该字段是必须的,它的值要么是请求Origin字段,要么是一个*,表示接受任意域名的请求。

Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

值得一提的是,如果想要CORS支持Cookie,不仅要在服务器指定HTTP响应报文首部字段,还需要在AJAX中打开withCredentials的属性。(jQuery中AJAX设置后面会讲到)

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

有些浏览器在省略withCredentials设置的时候,还是会发送Cookie。于是,可以显式关闭这个属性。

xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Acess-Control-Allow-Origin不能设置为*,必须设置成具体的域名,如果是本地调试的话可以考虑设置成null

Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段JavaScript脚本:

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

很明显,这是一个非简单请求,使用了PUT方法来发送请求,并且自定义了一个HTTP请求报文的首部字段。

于是,浏览器发现这是一个非简单的请求,就自动发出了一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的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

于是,服务器收到“预检”请求之后,检查了OriginAccess-Control-Request-MethodAccess-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回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他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

对比简单请求服务器响应的CORS字段,发现多了三个:

Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

于是,一旦浏览器通过了“预检”,以后每次浏览器正常的CORS请求,都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都有一个Access-Control-Allow-Origin头信息字段。如果开启了Cookie设置,那还有一个Access-Control-Allow-Credentials:true

postMessage跨域

iframe可以在父页面中嵌入一个子页面,在日常开发中一旦使用,避免不了的就要涉及到不同的iframe页面进行通信的问题,可能是获得其他iframe的DOM,或者是获取其他iframe上的全局变量或方法等等。

同源下的iframe,也就是iframe中的src属性的URL符合同源的条件,那么通过iframe的contentDocumentcontentWindow获取其他iframe的DOM或者全局变量、方法都是很简单的事情。

那如果是非同源的两个iframe,单纯的通过变量访问的方式就受到同源限制了。

为了解决这个问题,HTML5引入了一个新的API:postMessage,主要就是用来解决存在跨域问题的iframe页面之间通信的问题。

下面简单的举一个例子,假如现在有两个不同的页面,A页面的url是http://localhost:4002/parent.html,B页面的url的是http://localhost:4003/child.html,现在我把B页面用iframe嵌在A页面下面,代码(精简)是这样子的。现在我要实现的是向子页面B传递一个消息:

A页面代码:

<body>
    <h1>A页面</h1>
    <iframe src="http://localhost:4003/child.html" id="child">
    </iframe>
    <script>
        window.onload = function() {
            document.getElementById("child").contentWindow.postMessage("父页面发来贺电", "http://localhost:4003");
        }
    </script>
</body>

B页面代码:

<body>
    <h1>B页面</h1>
    <script>
        window.onload = function() {
            window.addEventListener("message", function(e) {
                //判断信息的来源是否来自于父页面,保证信息源的安全
                if(e.source != window.parent) return;
                alert(e.data);
            });
        };
    </script>
</body>

结果如图:
在这里插入图片描述
postMessage接受两个参数,一个是要传送的data,另外一个是目标窗口的源,如果想传给任何窗口,可以设置成*
目标页面接收信息的时候,使用的是window.addEventListener("message", function() {})

通过window.name跨域

window对象有个name属性,该属性有个特征:即在一个窗口的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置,并且可以支持非常长的 name 值(2MB)。由于安全原因,浏览器始终会保持 window.nameString 类型。

1.)a.html:(http://www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2.)proxy.html:(http://www.domain1.com/proxy.html)
中间代理页,与a.html同域,内容为空即可。

3.)b.html:(http://www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结:通过 iframesrc属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

location.hash + iframe跨域

因为父窗口可以对iframe进行URL读写,iframe也可以读写父窗口的URL,URL有一部分被称为hash,就是#号及其后面的字符,它一般用于浏览器锚点定位,Server端并不关心这部分,应该说HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变URL的hash部分来进行双向通信。每个window通过改变其他windowlocation来发送消息(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe),并通过监听自己的URL的变化来接收消息。这个方式的通信会造成一些不必要的浏览器历史记录,而且有些浏览器不支持onhashchange事件,需要轮询来获知URL的改变,最后,这样做也存在缺点,诸如数据直接暴露在了URL中,数据容量和类型都有限等。下面举例说明:

具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2.)b.html:(http://www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3.)c.html:(http://www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>
通过document.domain跨域

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

1.)父窗口:(http://www.domain.com/a.html)

<iframe id = "iframe" src="http://damonare.cn/b.html" onload = "test()"></iframe>
<script type="text/javascript">
    document.domain = 'damonare.cn';//设置成主域
    function test(){
        alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
    }
</script>

2.)子窗口:(http://child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

参考文献

前端常见跨域解决方案(全)
前端跨域整理
前端跨域解决方案

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值