头部背景图片
小畅童鞋的学习笔记 |
小畅童鞋的学习笔记 |

跨域常用解决方案

同源与同源策略

我们知道,同源指的是协议、域名、端口号全部相同。同源策略(Same Origin Policy)是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能都可能会受到影响。浏览器引入同源策略主要是为了防止XSS,CSRF攻击。
Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略是处于对用户安全的考量的,如果缺少了同源的限制,那又怎么能够确定别人的网站始终对你是友好的呢。

针对非同源的情况制定了一些限制条件:

  1. 无法读取不同源的cookie、LocalStorage、indexDB。
  2. 无法获得不同源的DOM。
  3. 不能向不同源的服务器发送Ajax请求。  

Image1.png
在浏览器中,script、img、iframe、link等标签都可以跨域加载资源,而不受同源策略的限制。事实上,在大多数情境下,我们经常是需要借用非同源来提供数据的,所以这就要用到跨域方面的技术了。

跨域解决方案

1.JSONP

JSONP是指JSON Padding,JSONP是一种非官方跨域数据交换协议,由于script的src属性可以跨域请求,所以JSONP利用的就是浏览器的这个原理,需要通信时,动态插入一个javascript标签。
请求的地址一般带有一个callback参数,假设需要请求的地址为 http://localhost:3000?callback=show, 服务器返回的代码一般是show()的JSON数据,而show函数恰恰是前端需要用的这个数据的函数。JSONP非常简单易用,自动补全API利用的就是JSONP。

一个简单的例子:
当远程数据一返回的时候,随着动态脚本的执行,这个handleResponse函数就会被执行。

var script = doxument.createElement("script");
script.setAttribute("type", "text/javascript");
script.src="http://example.com/ip?callback=handleResponse";
document.body.appendChild(script);

function handleResponse(data) {
    console.log('Your public IP address is: '+data.ip);
}

JSONP解决跨域的本质:script标签可以请求不同域名下的资源,即script请求不受浏览器同源策略的影响。上例中的script会向http://example.com/ 服务器发送请求,这个请求的url后面带了个callback参数,是用来告诉服务器回调方法的方法名的。因为服务器收到请求后,会把相应的数据写进handleResponse的参数,也就是服务器会返回如下的脚本:

handleResponse({
    "ip" : "8.8.8.8"
});

这样浏览器通过script标签下载的资源就是上面的脚本了,script标签下载完就会立即执行,也就是说http://example.com/ip?callback=handleResponse 这个请求返回后就会立即执行上面的脚本代码,而这个脚本代码就是调用回调方法和拿到json数据了。

缺点

  1. 只能发送get请求,无法发送post请求
  2. 无法判断请求成功还是失败

2.跨域源资源共享(CORS)  

  • CORS是W3C制定的跨站资源分享标准,可以让AJAX实现跨域访问,定义了在必须访问跨域资源时浏览器与服务器该如何沟通。CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器和服务器进行沟通,从而决定请求或响应应该成功还是失败。

  • 普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。

比如一个简单的使用GET或POST的请求,它没有自定义的头部,而主体内容是text/plain。在发送该请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名、端口号),以便服务器根据该头部信息来决定是否给予响应。

Origin: http://www.example.com

  如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中发回相同的源信息(如果是公共资源,可以发“*”)。例如:

Access-Control-Allow-Origin: http://www.example.com

  如果没有这个头部信息或信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。此时,请求和响应都不包含Cookie信息。

HTTP响应首部字段
Access-Control-Allow-Origin: <origin> | *
Access-Control-Expose-Headers: 头让服务器把允许浏览器访问的头放入白名单
Access-Control-Max-Age: 头指定了preflight请求的结果能够被缓存多久
Access-Control-Allow-Credentials: 头指定了当浏览器的credentials设置为true时是否允许浏览器读sponse的内容。
Access-Control-Allow-Methods: 首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。
Access-Control-Allow-Headers: 首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段

3.document.domain实现跨域

此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

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

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>

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

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

4.window.postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  1. 页面和其打开的新窗口的数据传递
  2. 多窗口之间消息传递
  3. 页面与嵌套的iframe消息传递
  4. 上面三个场景的跨域数据传递用法:

postMessage(data,origin)方法接受两个参数

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
  • origin: 协议+主机+端口号,也可以设置为”*”,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。

使用方法如下:
(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');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };
    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

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

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);
        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;
            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

5.window.name实现跨域

window.name 传输技术的基本原理
当在浏览器中打开一个页面,或者在页面中添加一个iframe时即会创建一个对应的window对象,当页面加载另一个新的页面时,window.name的属性是不会变的。这样就可以利用在页面动态添加一个iframe然后加载数据页面,在数据页面将需要的数据赋值给window.name。然而此时承载的iframe的parent页面还是不能直接访问不在同一域下的iframe的那么属性,这时,只需要将iframe再加载一个与承载页面同域的空白页面,即可对window.name进行数据读取。
通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

具体实现
http://www.domain1.com/a.html 主页面
http://http://www.domain2.com/b.html 数据页面
http://www.domain1.com/proxy.html 代理页面

  1. a.html:(http://www.domain1.com/a.html)
<script>
    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);
    });
</script>
  1. proxy.html:(http://www.domain1.com/proxy....
    中间代理页,与a.html同域,内容为空即可。

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

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

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

Lililich's Blog