前端基础 | 浅谈前端跨域

@author YogaZheng

鉴于本人目前正处于前端基础知识学习阶段,本文仅是在阅读了网络上诸多相关文章后的一个总结,加上一点自己写的小例子,更多的是用于自己的个人积累。看到这篇文章的朋友,我更建议大家去阅读本文参考资料中的文章(均附有链接),可以对跨域问题有更深刻的理解。

目录

同源与跨域

同源

同源策略

跨域

Cookie跨子域共享

iframe跨域窗口通信

document.domain

window.name(完整例子见附一)

location.hash

window.postMessage

AJAX跨域实现

JSONP

CORS

参考资料

附一:document.name在跨域中的应用举例


同源与跨域

同源

如果两个页面协议相同、域名相同、端口相同,则称这两个页面同源。相反的,如果两个页面的协议、域名、端口中有一个或多个不同,则称这两个页面非同源。

对于网址http://www.cityworks.cn:80而言,http://是协议,www.cityworks.cn是域名,80是端口(默认端口通常省略)。对于这个网址,其同源情况如下:

  • http://www,cityworks,cn/pages/newsInfo,html 同源,协议、域名、端口均相同

  • https://www.cityworks.cn:80 不同源,协议不同

  • http://www.yuque.com 不同源,域名不同

  • http://www.cityworks.cn:2333 不同源,端口不同

同源策略

简单来说,同源策略是限制了两个源之间资源交互的一种浏览器安全机制。对于非同源网页/网站,会受到三种行为限制:

  • 无法读取对方的Cookies、LocalStorage、IndexDB

  • 无法获得对方的DOM

  • 无法向对方发送AJAX请求

同源策略的制定是出于网络安全性考虑,防止恶意窃取数据,隔离潜在恶意文件。设想以下两种情景:

  • 你打开了银行账户页面,然后又"不小心"打开了一个恶意网站,如果没有同源策略,此时该恶意网站就可以通过javascript脚本"随心所欲地"访问、窃取、修改你的银行信息,包括账号密码。

  • 当你使用Cookie来维护用户的登录状态时(这也是我们现在经常做的),如果没有同源策略,这些Cookie信息就会泄露,其他网站就可以冒充这个登录用户。

跨域

如果两个页面非同源,我们试图进行这两个页面通信的行为称为跨域。

这里值得一提的是,跨域并非是浏览器限制了发起跨域请求,而是跨站请求可以正常发起,但返回结果被浏览器拦截了。也有特例,比如Chrome和Firefox浏览器对于从HTTPS协议访问HTTP协议的跨域请求在其未发出时就拦截。

在实际的项目开发过程中,我们总会不可避免的要进行跨域请求操作。但是,对于协议和端口不同的跨域问题,前端是无法解决的,需要通过后台实现。 所以通常而言,前端所说的跨域处理指的是对于不同域名通信的跨域实现,也就是本文讨论的主要内容。

 

Cookie跨子域共享

我们知道,由于同源策略的限制,Cookie只有同源的页面才能共享。但是,如果两个页面主域名相同,子域名不同,浏览器允许通过设置docuement.domain共享Cookie和DOM。

举个栗子,A页面地址是http://a.example.com/a.html,B页面地址是http://b.example.com/b.html,那么只要两个页面将各自的document.domain指向同一主域example.com,它们就可以共享Cookie。

在A页面设置document.domain,并通过脚本设置一个Cookie:

document.domain = 'example.com';
document.cookie = 'favourite_food=chocolate';

在B页面设置相同的document.domain,就可以读取到这个Cookie:

document.domain = 'example.com';
console.log(document.cookie); //包括'favourite_food=chocolate'

另外,服务器也可以在设置Cookies的时候,指定Cookies的所属域名为主域名:

Set-Cookie: key=value; domain=.example.com; path=/

如此,二级、三级域名不用做任何设置,就都可以读取这个Cookie。

 

iframe跨域窗口通信

项目中会有使用iframe把其他域名的内容嵌入页面中的场景(比如登录/注册等表单提交浮窗),有时候会需要与父窗口进行通信。

document.domain

如果打开窗口的主域与父窗口主域相同,子域名不同,那么同Cookie一样,可以使用document.domain进行通信,获取彼此的DOM。

举个栗子:在A页面http://a.example.com/a.html中有一个<iframe>标签,它的src属性的值是http://b.example.com/b.html,对应B页面,它们有共同的主域example.com,那么只要两个页面将document.domain设置为这一主域,就可以相互访问DOM。

父窗口a.html:

<iframe id="iframe" src="http://b.example.com/b.html"></iframe>
<ul id="arms">
    <li>陈情</li>
    <li>避尘</li>
</ul>
<script type="text/javascript">
    document.domain = 'example.com';
    window.onload = function() {
        var iframe = document.getElementBtId('iframe');
        var childDoc = iframe.contentWindow.document;
        console.log(doc.getElementById('cp')); // 魏无羡 蓝忘机
    }
</script>

子窗口b.html:

<ul id="cp">
    <li>魏无羡</li>
    <li>蓝忘机</li>
</ul>
<script type="text/javascript">
    var parentDoc = window.parent.document;
    console.log(doc.getElementById('arms')); // 陈情 避尘
</script>

window.name(完整例子见附一)

window.name有一个特征:在一个窗口(window)的生命周期内,窗口载入的所有页面都是共享一个window.name的,且每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。也就是说,无论是否同源,只要在同一个窗口中,前一个页面设置了这个属性,后一个页面可以读取。

基于这个特征,我们可以实现两个域名完全不同的页面之间的通信,只要它们在同一个窗口内先后打开。

举个栗子,我们新建标签页,在地址栏输入https://www.baidu.com,在控制台输入:

window.name = 'Superman';

然后在地址栏输入http://www.cityworks.cn,在控制台查看window.name:

>>    window.name
<<    "Superman"

由此我们可以实现跨域页面的通信。

结合iframe,父窗口可以获取到子窗口下的window.name。

首先,父窗口http://parent.url.com/a.html载入了不同源的子窗口http://child.path.com/index.html:

<iframe id="iframe" src="http://child.path.com/index.html"></iframe>

接着,使子窗口跳回一个与主窗口同域的网页http://parent.url.com/b.html,此时该窗口的window.name不变:

document.getElementById('iframe').src = "http://parent.url.com/b.html;

如此,父窗口就可以通过读取此时子窗口的window.name,获取非同源页面的window.name了:

console.log(document.getElementById('iframe').contentWindow.name);

另外,结合iframe或window.open,window.name可以实现父窗口向子窗口传递数据。

在iframe中,只需指定标签的name属性即可:

<iframe src="http://child.path.com" name="Thor"></iframe>

然后根据上述父窗口获取子窗口的window.name的方式,我们可以发现,此时子窗口的window.name为"Thor"。

在window.open中,只需指定target即可:

window.open('http://child.path.com', 'Loki');

此时在打开窗口的控制台可以看到其window.name:

>>  window.name 
<<  "Loki"

window.name在跨域使用上需要注意:

1.window.name仅支持string类型的数据,其他数据类型都会被强制转换为string。

>>    window.name = 123
<<    "123"
>>    window.name = ['iron-man','captain-america']
<<    "irom-man,captain-america"
>>    window.name = {name: 'black-widow'}
<<    "[object Object]"
>>    window.name = null
<<    "null"
>>    window.name = undefined
<<    "undefined"

2.window.name传递数据大小限制一般为2M,不同浏览器有一定差异。

location.hash

对于网址http://example.com/index.html#ant-man,我们称该url的#号后面部分为片段标识符,片段标识符不会被发送到服务器端,不会引起页面刷新。利用片段标识符,我们可以把传递的数据依附在url上,实现非同源父窗口与子窗口之间的通信。显然,用片段标识符传递数据的方法既适用于iframe标签,也适用于window.open打开窗口传递数据。

举个栗子,父窗口向<iframe>子窗口传递数据,将数据写入子窗口的片段标识符:

<iframe id="iframe" src="http://child.example.com/b.html#ant-man"></iframe>

子窗口可以通过监听事件检测片段标识符的变化:

window.onhashchange = checkData;
function checkData() {
    var data = location.hash;
    console.log(data); // "#ant-man"
}

同window.name类似,子窗口要通过location.hash向父窗口传递数据,需要在子窗口中再嵌入与父窗口同源的第三个窗口,将信息设置在第三个窗口的hash值上,然后第三个窗口改变父窗口的hash值,从而实现跨域。通过location.hash实现子窗口向父窗口传递数据的方法比较复杂,通常不做考虑。

location.hash在跨域使用上需要注意:

1.同window.name一样,location.hash仅支持string类型的数据。

2.location.hash字段是加在URL后的,因此受到URL长度限制,不同浏览器限制不同,如IE浏览器限制最长URL为2083个字符,Google Chrome限制为8182个字符。

window.postMessage

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

otherWindow.postMessage(message, targetOrigin)

otherWindow

其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。

message

将要发送到其他Window的数据,支持类型String、Object。

targetOrigin

用于指定哪些窗口能接收到数据,其值可以是字符串"*"(表示任意窗口)或一个URI。

举个栗子,父窗口http://parent.url.com向子窗口http://child.path.com发送数据,使用postMessage方法:

var child = window.open('http://child.path.com');
child.postMessage('Wonder Woman', '*');

相反的,子窗口向父窗口发送数据:

window.opener.postMessage('Wolverine', '*');

进一步的,父窗口和子窗口都可以通过message事件,监听对方的消息。

window.addEventListener('message', function(event) {
    console.log(event);
});

message事件的事件对象event提供三个属性:

  • event.source:发送消息的源窗口

  • event.origin:消息发送指向的网址

  • event.data:消息内容

举个栗子,子窗口可以通过event.source属性引用父窗口,从而使用postMessage向父窗口发送信息:

window.addEventListener('message', function(e) {
    event.source.postMessage('Deadpool')
}

 

AJAX跨域实现

AJAX由于同源策略的限制,只能向同源的网址发送请求。为了突破这个限制,除了假设服务器代理(浏览器请求同源服务器,再由后者请求外部服务)以外,还可以通过JSONP和CORS方法向非同源服务器发送请求。

JSONP

我们知道,凡是拥有src属性的标签(如<script>、<img>、<iframe>等)都不受同源策略的限制,拥有跨域请求静态资源的能力。JSONP正是利用了<script>标签的这个特性实现跨域资源请求。

JSONP(JSON with Padding,填充式JSON)是一种非官方跨域数据交互协议,允许一个域(以下称客户端)传递一个callback参数给另一个域(以下称服务端),服务端用这个callback参数作为函数名包裹JSON数据并返回给客户端,如此,客户端就可以进一步处理返回的数据。

JSONP具体实现过程:

1.客户端在页面添加一个<script>标签。

2.将请求接口地址作为创建的<script>标签src属性的值,其中关键是设置回调函数名作为请求接口的参数,构建一个JSONP请求。

3.服务端接收到请求后,通过参数获得回调函数名,在JSON数据外添加函数包裹层,再返回给客户端。

4.客户端获得的返回数据,实际上是处理函数的调用,进行数据处理。

举个栗子,调用豆瓣读书API获取JSON数据:

客户端js代码:

// 创建script标签
var script = document.createElement('script');
script.type = 'text/javascript';
// src属性的值为接口地址,同时传递参数指定回调函数名
script.src = 'https://api.douban.com/v2/book/26763013?callback=onBack';
// 插入script标签
document.head.appendChild(script);

// 回调函数
function onBack(data) {
    console.log(data);
}

服务端返回数据:

onBack({
    "author": ["墨香铜臭"],
    "pubdate": "2016-12-8",
    "alt": "https://book.douban.com/subject/26763013/",
    "id": "26763013",
    "publisher": "平心工作室",
    "title": "魔道祖师",
    "url": "https://api.douban.com/v2/book/26763013",
    "summary": "前世的魏无羡万人唾骂,声名狼藉。被护持一生的师弟带人端了老巢,纵横一世,死无全尸。曾掀起腥风血雨的一代魔道祖师,重生成了一个……脑残。还特么是个人人喊打的断袖脑残!我见诸君多有病,料诸君见我应如是。但修鬼道不修仙,任你千军万马,十方恶霸,九州奇侠,高岭之花,但凡化为一抔黄土,统统收归旗下,为我所用,供我驱策!高贵冷艳闷骚攻×邪魅狂狷风骚受"
});

注意:

  • 返回的JSON数据被视为JavaScript对象而非字符串,从而避免了使用JSON.parse。

  • <script>不仅可以访问 JSONP接口,也可以访问普通接口或 js文件,它们的返回数据是有区别的,因此如果接口要做 JSONP兼容,需要判断是否对应 callback关键字参数。

利用JSONP,我们可以实现AJAX的跨域请求。

原生JavaScript实现:

ajax({ 
    url: 'http://a.example.com/api',  // 请求地址
    jsonp: 'onBack', // 采用jsonp请求,且回调函数名为"onBack"
    data: {'name': 'joker'}, // 传输数据
    success: function(res){
        console.log(res); 
    },
    error: function(error) {
        console.log(error)
    }
});

function ajax(params) {  
    //创建script标签并加入到页面中 
    var head = document.getElementsByTagName('head')[0]; 
    params.data['callback'] = params.jsonp; 
    var script = document.createElement('script'); 
    head.appendChild(script);  
      
    //创建jsonp回调函数 
    window[params.jsonp] = function(json) { 
        head.removeChild(script); 
        window[params.jsonp] = null; 
        params.success && params.success(json); 
    };

    //发送请求 
    script.src = params.url + '?' + params.data;  
};  

jQuery对ajax的JSONP跨域实现进行了封装,只需指定dataType:

$.ajax({
    url: 'http://a.example.com/api',
    type: 'GET',
    dataType: 'jsonp',
    success: function(res) {
        console.log(res.data);
    }
)

基于JSONP的实现原理,我们可以看出JSONP只能发起GET请求,不能进行较为复杂的POST或其他请求。因此对于复杂的请求,现在通常考虑用CORS解决跨域问题。

CORS

CORS(Cross-origin resource sharing,跨域资源共享)是一个W3C标准,允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS的实现原理如下图所示:

  • 客户端发出CORS请求,浏览器首先判断请求是否同时满足以下两大条件,将请求分为简单请求和非简单请求:

    1. 请求方法是以下三种方法之一:

      • HEAD

      • GET

      • POST

    2. HTTP的头信息不超出以下几种字段:

      • Accept

      • Accept-Language

      • Content-Language

      • Last-Event-ID

      • Content-Type:只限三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

    凡是不同时满足上面两个条件的,就属于简单请求。

  • 判断请求类型后,浏览器对不同的请求各自进行一定的处理。

    • 对于简单请求,浏览器会在头信息中增加一个Origin字段,用于说明本次请求来自于那个源。

    • 对于非简单请求,浏览器会发送一个OPTION请求进行预检,向服务器询问是否允许跨源请求,若不允许,则返回不含CORS相关字段信息的HTTP响应。

  • 简单请求增加了Origin字段、非简单请求通过预检之后,向服务端发送正式CORS请求。服务端会对Origin字段信息进行检查,判断Origin指定的源是否在请求许可范围之内。

    • 若Origin指定域名在许可范围内,服务器返回正确的数据及头部信息,头部信息字段中包含三个与CORS请求相关的字段:

      • Access-Control-Allow-Origin:必须,Origin字段的值或"*"

      • Access-Control-Allow-Credentials:可选,表示是否允许发送Cookie

      • Access-Control-Expose-Headers:可选,指定需要返回的其他字段

    • 若Origin指定域名不在许可范围内,服务器返回一个正常的HTTP相应,但头部信息不包含Access-Control-Allow-Origin等CORS相关字段。

  • 浏览器通过判断返回的响应头部是否包含Access-Control-Allow-Origin字段判断CORS请求是否成功。若失败,则抛出错误。

简单来说,CORS的跨域原理可以理解为,服务器通过增加响应头字段来"告诉"浏览器(注意同源策略是浏览器安全机制)这是一个符合标准的ajax跨域请求,使浏览器"同意"获取数据返回给客户端。

原理讲完,举个栗子。

以本地Nodejs(Express框架)服务为例,在app.js文件中为响应头部添加CORS对应字段:

var express = require('express');
var app = express();

...

app.all('*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("X-Powered-By", ' 3.2.1')
    if (req.method == 'OPTIONS') {
        res.send(200); // 让options请求快速返回
    }
    else {
        next();
    }
});

...

app.listen(3000);

如此,前端页面只需直接访问接口地址即可。

$.ajax({
    url: 'http://localhost:3000/getData',
    type: 'get',
    success: function (data){
        console.log(data);
    },
    error: function (err) {
        console.log(err);
    }
})

 

参考资料

  1. 阮一峰:浏览器同源政策及其规避方法

  2. 阮一峰:跨域资源共享CORS详解

  3. 丹丹赵:面试--同源以及规避同源限制的方法

  4. MuffinFish:同源策略及前后端(同源/跨域)通信

  5. vczhan:新手学跨域之iframe

  6. 祝仙森:前端跨域及其解决方案

  7. trigkit4:详解js跨域问题

  8. 韩子迟:利用location.hash+iframe跨域获取数据详解

  9. Lichun Dai:ajax跨域,这应该是最全的解决方案了

  10. 刘小光:JSONP和CORS实现跨源请求

  11. 一只瓦罐:同源策略与JS跨域(JSONP,CORS)

 

附一:document.name在跨域中的应用举例

父窗口页面window_name.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <title>父窗口</title>
</head>

<body>
<h2>这是父窗口</h2>
<!-------------------- Part-1 ------------------->
<!-- Start iframe父窗口获取非同源子窗口window.name -->
<div>
    <h3>此部分演示父窗口获取非同源子窗口的window.name</h3>
    <iframe id="iframe1" src="https://www.yuque.com/"></iframe>
    <button onclick="changeIframe1()">点击变换iframe内容</button>
    <button onclick="checkIframe1()">点击查看iframe的window.name</button>
</div>
<!-- End iframe父窗口获取非同源子窗口window.name -->

<!---------------------- Part 2 ---------------------------->
<!-- Start Part-2 iframe父窗口设置非同源子窗口window.name的值 -->
<div>
    <h3>此部分演示父窗口设置非同源子窗口的window.name的值</h3>
    <iframe id="iframe2" src="https://www.baidu.com/" name="Thor & Loki"></iframe>
    <button onclick="changeIframe2()">点击变换iframe内容</button>
    <button onclick="checkIframe2()">点击查看iframe的window.name</button>
</div>
<!-- End iframe父窗口设置非同源子窗口window.name的值 -->
</body>

<script type="text/javascript">
    /***************** Part-1 *****************/
    // 改变iframe子窗口内容为同源页面
    function changeIframe1() {
        document.getElementById('iframe1').src = './window_name_child.html';
    }
    // 获取子窗口window.name
    function checkIframe1() {
        try {
            alert('非同源子窗口window.name设置成功:' + document.getElementById('iframe1').contentWindow.name);
        } catch (e) {
            alert('出现跨域咯!错误信息:' + e.message);
        }
    }

    /****************** Part-2 ******************/
    // 改变iframe子窗口内容为同源页面
    function changeIframe2() {
        document.getElementById('iframe2').src = './window_name_child.html';
    }
    // 获取子窗口window.name
    function checkIframe2() {
        try {
            alert('非同源子窗口window.name设置成功:' + document.getElementById('iframe2').contentWindow.name);
        } catch (e) {
            alert('出现跨域咯!错误信息:' + e.message);
        }
    }
</script>
</html>

同源子窗口页面window_name_child.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <title>同源子窗口</title>
</head>

<body>
<h2>这是与父窗口同源的子窗口</h2>
</body>
</html>

猜你喜欢

转载自blog.csdn.net/weixin_41872030/article/details/82465594