前端跨域深入理解

Posted 怪诞咖啡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端跨域深入理解相关的知识,希望对你有一定的参考价值。

 

同源策略(跨域的由来)

同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。

“同源”是指:协议相同、域名相同、端口相同

同源政策的目的:是为了保证用户信息的安全,防止恶意的网站窃取数据。

同源的限制范围:

1. Cookie、LocalStorage 和 IndexDB 无法读取

2. DOM 无法获得

3. AJAX 请求不能发送

这里需要明确的一点是:所谓的域跟js的存放服务器没有关系,比如baidu.com的页面加载了google.com的js,那么此js的所在域是baidu.com而不是google.com。也就是说,此时该js能操作baidu.com的页面对象,而不能操作google.com的页面对象。

跨域的方法

一、使用JSONP跨域(单项跨域-一般用于获取数据)

原理:因为通过script标签引入的js是不受同源策略的限制的(正如前文提到的baidu.com的页面加载了google.com的js)。所以我们可以通过script标签引入一个js或者是一个其他后缀形式(如php,jsp等)的文件,此文件返回一个js函数的调用,如返回一个对象:

JSONP_getPerson({

    "name":"怪诞咖啡",

    "age":18,

    "job":"前端攻城狮",

 });

也就是说此文件返回的结果调用了JSONP_getPerson函数,并且把{"name":"怪诞咖啡","age":18,"job":"前端攻城狮"}传进去,这{"name":"怪诞咖啡","age":18,"job":"前端攻城狮"}是一个对象。此时我们的页面中有一个JSONP_getPerson函数,函数JSONP_getPerson就被调用到,并且传入了一个对象。此时就实现了在本域获取其他域数据的功能,也就是跨域。

JSONP_getPerson:前端引入远程js并定义好JSONP_getPerson函数,注意需要先定义好JSONP_getPerson函数,避免在远程js加载完成并调用JSONP_getPerson时,此函数不存在

例子代码:

<script>

function JSONP_getPerson(users) {

    console.dir(users);

}

</script>

<script src="http://xxx/index.php"></script>

// 前端代码调用script标签块必须在函数标签块之后

地址说明:http://xxx/index.php,其中的xxx是表示不同域名下的文件,测试的时候,把前端和后端代码放到不同的源中测试

<?php

echo ‘JSONP_getPerson({

"name":"怪诞咖啡",

"age":18,

"job":"前端攻城狮",

})‘;//返回一个js函数的调用

?>

为什么script标签引入的文件不受同源策略的限制?

  1. 因为script标签引入的文件内容是不能被客户端的js获取到的,不会影响到被引用文件的安全,所以没必要使script标签引入的文件遵循浏览器的同源策略。
  2. 通过ajax加载的文件内容是能够被客户端js获取到的,所以ajax必须遵循同源策略,否则被引入文件的内容会泄漏或者存在其他风险。

JSONP的缺点则是:

  1. 它只支持GET请求而不支持POST等其它类型的HTTP请求(虽然采用post+动态生成iframe是可以达到post跨域的目的,但这样做是一个比较极端的方式,不建议采用)。明确说明:jquery使用POST请求jsonp可以成功是由于jquery自动把POST转化成GET,实际还是GET请求
  2. 一般get请求能完成所有功能。如:如果需要给其他域服务器传送参数可以在请求后挂参数(注意不要挂隐私数据),即 <script src="http://xxx/getPerson.php?name=Hello&age=18"></script> 

地址说明:http://xxx/getPerson.php?name=Hello&age=18,其中的xxx是表示不同域名下的文件,测试的时候,把前端和后端代码放到不同的源中测试

感悟:

  1. JSONP易于实现,但是也会存在一些安全隐患,如果第三方的脚本随意地执行,那么它就可以篡改页面内容,截获敏感数据。
  2. 在受信任的双方传递数据,JSONP是非常合适的选择。
  3. 可以看出来JSONP跨域一般用于获取其他域的数据。
  4. 一般能够用JSONP实现跨域就用JSONP实现,这也是前端用的最多的跨域方法。

二、动态创建script标签(单项跨域-一般用于获取数据)

这种方法其实是JSONP跨域的简化版,JSONP只是在此基础上加入了回调函数。比如上例中的 getPerson.php 返回的如果不是一个js函数的调用,而是一个js变量,如:

<?php echo ‘var person = {"name":"怪诞咖啡","age":18,"job":"前端攻城狮"}‘ ?>

那么在当前域下就可以取到person变量,这里需要注意判断script节点是否加载完毕,如:

<script>

var head= document.getElementsByTagName(‘head‘)[0];

var scriptPerson= document.createElement(‘script‘);

scriptPerson.type= ‘text/javascript‘;

scriptPerson.src= ‘http://xxx/index.php‘;

head.appendChild(scriptPerson);

 

scriptPerson.onload = scriptPerson.onreadystatechange = function() {

    if (!this.readyState || this.readyState === ‘loaded‘ || this.readyState === ‘complete‘) {

        console.log(person); //此处取出其他域的数据

        scriptPerson.onload = scriptPerson.onreadystatechange = null;

    }

};

</script>

地址说明:http://xxx/index.php,其中的xxx是表示不同域名下的文件,测试的时候,把前端和后端代码放到不同的源中测试

三、window.name属性(单项跨域-一般用于获取数据)

来源:

  1. window.name 传输技术,原本是 Thomas Frank 用于解决 cookie 的一些劣势(每个域名 4 x 20 Kb的限制、数据只能是字符串、设置和获取 cookie 语法的复杂等等)而发明的。后来 Kris Zyp 在此方法的基础上强化了 window.name 传输 ,用来解决跨域数据传输问题。
  2. window.name 的美妙之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

特征:

  1. 浏览器窗口有 window.name 属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。
  2. 即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

基本原理:

  1. 当在浏览器中打开一个页面,或者在页面中添加一个iframe时,即会创建一个对应的window对象,当页面加载另一个新的页面时,window的name属性是不会变的。
  2. 这样就可以利用在页面动态添加一个iframe然后src加载数据页面,在数据页面将需要的数据赋值给window.name。
  3. 然而此时承载iframe的index页面(也就是请求数据的页面)还是不能直接访问,不在同一域下iframe的name属性,这时只需要将iframe再加载一个与承载页面同域的空白页面,即可对window.name进行数据读取

例子:(index.html经过3秒跳转到data.html)

<script>

// 页面index.html

window.name =‘我是index.html页面的window.name值‘;

setTimeout(function(){

window.location = ‘http://xxx/data.html‘;

},3000);

</script>

// 页面data.html,两者不同域名

<script>

console.log(window.name);

</script>

代码说明:实际运行中能够看到在 data.html 页面上成功获取到了,它的上一个页面 index.html 给window.name设置的值。如果在之后所有载入的页面都没对 window.name 进行修改的话,那么所有这些页面获取到的 window.name 的值都是 index.html 页面设置的那个值。当然,如果有需要,其中的任何一个页面都可以对window.name的值进行修改。注意, window.name 的值只能是字符串的形式,这个字符串的大小最大能允许2M左右,具体情况取决于不同的浏览器,但一般是够用了。

上面的例子中,我们用到的页面 index.htmldata.html 在同域和不同域环境下都进行了测试,结果都一样,这也正是利用window.name进行跨域的原理。

问题:这样获取到的 window.name 的值需要跳转页面获取,自然不是我们想要的结果,我们想要的是页面不跳转也可以获取到数据,上面的例子是为了体现和理解 window.name 的跨域能力,这种简单的方法才更有利于初学者理解和学习。

实现不跳转请求数据:接下来我们运用 iframe+window.name 来实现,页面不跳转来获取数据,方法就是:在 index.html 页面中使用一个隐藏的 iframe 来充当一个中间人的角色,由 iframe 去获取 data.index 的数据,然后 index.html 再去得到 iframe 获取到的数据。

例子 => 三个文件:

  1. index.html是主文件,用于请求数据的主文件
  2. empty.html是空文件,是代理文件,用于过渡使用
  3. data.html是请求数据的文件

index.html和empty.html必须在同一域,data.html为其他域的数据文件

<script>

// index.html

var state = 0,

    iframe = document.createElement(‘iframe‘),

    loadfn = function() {

        if (state === 1) {

            var data = iframe.contentWindow.name; // 读取数据

            console.log(data); // 打印出‘{"name":"怪诞咖啡","age":18,"job":"前端攻城狮"}‘,是字符串类型

        } else if (state === 0) {

            state = 1;

            iframe.contentWindow.location = "empty.html"; // 设置的代理文件

        }

    };

iframe.src = ‘http://xxx/data.html‘;

if (iframe.attachEvent) {

    iframe.attachEvent(‘onload‘, loadfn);

} else {

    iframe.onload = loadfn;

}

document.body.appendChild(iframe);

</script>

<script>

// data.html

window.name = ‘{"name":"怪诞咖啡","age":18,"job":"前端攻城狮"}‘;

</script>

地址说明:http://xxx/data.html,其中的xxx是表示不同域名下的文件,测试的时候,把前端和后端代码放到不同的源中测试

我在最初探索window.name+iframe的时候遇到的坑:index.htmlempty.html 没有体现在同一域中(比如页面地址使用 http://localhost/index.html ,里面iframe 的跳转地址使用 iframe.contentWindow.location = ‘http://127.0.0.1/empty.html‘)是不可以的,检测为不是同一个域,原因请看下面介绍

localhost与127.0.0.1的区别是什么?

localhost也叫local ,正确的解释是:本地服务器

127.0.0.1在windows等系统的正确解释是:本机地址(本机服务器)

他们的解析通过本机的host文件,windows自动将localhost解析为127.0.0.1

  • localhot(local)是不经网卡传输!这点很重要,它不受网络防火墙和网卡相关的的限制。
  • 127.0.0.1是通过网卡传输,依赖网卡,并受到网络防火墙和网卡相关的限制。
  • 一般设置程序时本地服务用localhost是最好的,localhost不会解析成ip,也不会占用网卡、网络资源。
  • 有时候用localhost可以,但用127.0.0.1就不可以的情况就是在于此。猜想localhost访问时,系统带的本机当前用户的权限去访问,而用ip的时候,等于本机是通过网络再去访问本机,可能涉及到网络用户的权限。

双向跨域:两个iframe之间或者两个页面之间,一般用于获取对方数据,document.domain方式还可以直接操作对方DOM

四、document.domain(两个iframe之间或者相同一级域名不同的二级域名cookie传递,属于双向跨域)

cookie:

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie。

举例来说,A网页是 http://w1.example.com/a.html ,B网页是 http://w2.example.com/b.html ,那么只要设置相同的 document.domain ,两个网页就可以共享Cookie。

两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域

iframe:

和cookie同理,都是需要设置,document.domain

<script>

document.domain = ‘example.com‘;

</script>

问题:1、安全性,当一个站点被攻击后,另一个站点会引起安全漏洞。2、如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。

五、window.postMessage(两个iframe之间或者两个页面之间,属于双向跨域)

HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。

这个API为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源。

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain 设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

六、location.hash(两个iframe之间,属于双向跨域),又称FIM,Fragment identifier Messaging的简写

概念:片段标识符(fragment identifier)指的是,URL的#号后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。

下面是hash不刷新页面,更新hash的例子:

setTimeout(function(){

    location.href= ‘index.html‘ + "#" + ‘{"name":"怪诞咖啡","age":18,"job":"前端攻城狮"}‘;

},1000);

window.onhashchange = checkMessage;

 

function checkMessage() {

  var message = window.location.hash;

  console.log(message.slice(1)); //{"name":"怪诞咖啡","age":18,"job":"前端攻城狮"}

}

hash实现跨域的方式:两个文件,一个 index.html 文件,一个是不同域名下的 index.php 文件

// index.html

<script>

function getData(url, fn) {

    var iframe = document.createElement(‘iframe‘);

    iframe.style.display = ‘none‘;

    iframe.src = url;

 

    iframe.onload = function() {

        fn(iframe.contentWindow.location.hash.substring(1));

        window.location.hash = ‘‘;

        document.body.removeChild(iframe);

    };

 

    document.body.appendChild(iframe);

}

 

// get data from server

var url = ‘http://127.0.0.1/index.php‘;

getData(url, function(data) {

    var jsondata = JSON.parse(data);

    console.log(jsondata);

});

</script>

// index.php

<?php

$data = ‘{\"name\":\"怪诞咖啡\",\"age\":18,\"job\":\"前端攻城狮\"}‘;

echo

"

<script>

window.location = ‘http://localhost/index.html‘ + ‘#‘ + \"$data\";

</script>

"

?>

如果看懂了之前利用 window.name+iframe 跨域获取数据,那么使用 window.hash+iframe 也就很好理解了。一样都是动态插入一个iframe,然后把iframe的src指向服务端地址,而服务端同样都是输出一段js代码,同样都是利用和子窗口之间的通信完成数据传输,同样要针对同源策略做出处理。

HTML标签和跨域

看到之前的介绍,能够了解到跨域都是通过HTML标签做一些事情,HTML有:script、img、iframe、link

CSS中,有伟大的 background 属性,也可以实现跨域

跨域的方法,可以说很多很多,不仅仅局限于上面的方法,今后慢慢来总结这块

以上是关于前端跨域深入理解的主要内容,如果未能解决你的问题,请参考以下文章

深入理解跨域SSO(单点登录)原理与技术

深入浅出让你理解跨域与SSO单点登录原理与技术

从Spring-Boot开始深入理解Spring系列——Spring-Boot处理web开发的跨域问题

CSS移动前端开发之viewport的深入理解

深入理解行内元素的布局

移动前端开发之viewport的深入理解