“跨域”可以说是web开发中最经常遇到的问题之一,虽然比较简单,也容易比较解决。但是从这个问题却可以了解到相当多的知识点。这里也详细总结一下吧。
什么是跨域
由于安全的原因,浏览器做了很多方面的工作,其中之一就是同源策略。
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 Js对象无法获得
- AJAX 请求不能发送
由此也就引入了一系列的跨域问题,需要注意的是:
跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从HTTPS的域跨域访问HTTP,比如Chrome和Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例
为什么要有跨域
AJAX同源策略主要用来防止CSRF攻击。如果没有AJAX同源策略,相当危险,我们发起的每一次HTTP请求都会带上请求地址对应的cookie,那么可以做如下攻击:
1.用户登录了自己的银行页面 http://mybank.com,http://mybank.com向用户的cookie中添加用户标识。
2.用户浏览了恶意页面 http://evil.com。执行了页面中的恶意AJAX请求代码。
3.http://evil.com向http://mybank.com发起AJAX HTTP请求,请求会默认把http://mybank.com对应cookie也同时发送过去。
4.银行页面从发送的cookie中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了。
5.而且由于Ajax在后台执行,用户无法感知这一过程。
DOM同源策略也一样,如果iframe之间可以跨域访问,可以这样攻击:
1.做一个假网站,里面用iframe嵌套一个银行网站 http://mybank.com。
2.把iframe宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
3.这时如果用户输入账号密码,我们的主网站可以跨域访问到http://mybank.com的dom节点,就可以拿到用户的输入了,那么就完成了一次攻击。所以说有了跨域跨域限制之后,我们才能更安全的上网了。
跨域解决方案
在实际开发中,因为业务问题,我们的应用可能会分布在不同的域名或端口上,就会遇到同源策略的限制,解决这个问题有以下几种方案。
jsonp
原理
虽然名字叫做jsonp
,实际上它的原理和json并没有关系。主要是利用标签的src
属性没有受同源策略限制来获取来自其他域名的数据。我们通常使用 img / script 标签。最常用的就是 script 标签。因为js是自已执行其他域的函数的,而通过给函数传递参数,就可以达到跨域的目的。
使用方式
首先我们首先定义一个函数。
function callback(data){
console.log(data);
}
然后引入一个其他域的js文件如:static.lscho.com/run_callbac…,在这个文件中调用 callback
函数,并传入参数,我们就得到了来自其他域的数据。
callback({title:'jsonp演示'});
以上代码已插入本文页面中,打开控制台即可看到数据
利用这一点,我们可以给这个函数传入任意参数。所以我们后台就可以返回 callback(+任意数据+)
来达到跨域目的。
因为jquery对jsonp的包装,如下。
$.ajax({
method: 'jsonp',
url: 'http://example2.com',
success: function(data) {
console.log(data)
}
})
导致很多人认为jsonp是ajax相关的东西,其实并不是。Ajax 是利用 XMLHTTPRequest 来请求数据的,会受到同源限制。只是jquery为了api的一致性,对上述过程进行了包装。
jsonp也存在很多缺点,比如只支持get方式的请求,无法双向传输数据等。
window.name
原理
window对象中name参数是可以在多窗口(标签)内共享的,利用这一点,我们可以传输数据。结合 iframe 就可以有更为强大的功能。比如打开一个iframe并设置地址外域,在外域名设置window.name
,再跳转回本域,此时还能获取到外域设置的window.name
,此时就达到了跨域的目的。传输的数据,大小一般为2M,IE和firefox下可以大至32M左右,数据格式可以自定义。
使用方式
//a.html
<script type="text/javascript">
var state = 0,
iframe = document.createElement('iframe'),
loadfn = function() {
if (state === 1) {
var data = iframe.contentWindow.name; // 读取数据
alert(data); //弹出'I was there!'
} else if (state === 0) {
state = 1;
iframe.contentWindow.location = "http://a.com/proxy.html"; // 设置的代理文件
}
};
iframe.src = 'http://b.com/b.html';
if (iframe.attachEvent) {
iframe.attachEvent('onload', loadfn);
} else {
iframe.onload = loadfn;
}
document.body.appendChild(iframe);
</script>
//proxy.html
//空文件放在a.com下起中转作用即可
//b.html
<script type="text/javascript">
window.name = 'I was there!'; // 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右,数据格式可以自定义,如json、字符串
</script>
缺点是比较麻烦,而且需要创建iframe,容易被xx浏览器拦截。
document.domain
原理
原理和window.name
相似,在不同的子域 + iframe交互的时候,获取到另外一个 iframe 的 window对象是没有问题的,但是获取到的这个window的方法和属性大多数都是不能使用的。这种现象可以借助document.domain 来解决。
使用方式
//a.com
<iframe id='i' src="b.com" onload="do()"></iframe>
<script>
document.domain = 'a.com';
document.getElementById("i").contentWindow;
</script>
//b.com
<script>
document.domain = 'a.com';
</script>
这样,就可以解决问题了。值得注意的是:document.domain 的设置是有限制的,只能设置为页面本身或者更高一级的域名。
使用比较方便,但是如果一个网站被攻击之后另外一个网站很可能会引起安全漏洞。
location.hash
原理
这种方法可以把数据的变化显示在 url 的 hash 里面。但是由于 chrome 和 IE 不允许修改parent.location.hash 的值,所以需要再加一层。
使用方式
例:a.html 和 b.html 进行数据交换。
//a.html
function startRequest(){
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://2.com/b.html#paramdo';
document.body.appendChild(ifr);
}
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {
console.log('Now the data is '+data);
}
} catch(e) {};
}
setInterval(checkHash, 2000);
//b.html
//模拟一个简单的参数处理操作
switch(location.hash){
case '#paramdo':
callBack();
break;
case '#paramset':
//do something……
break;
}
function callBack(){
try {
parent.location.hash = 'somedata';
} catch (e) {
// ie、chrome的安全机制无法修改parent.location.hash,
// 所以要利用一个中间域下的代理iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://3.com/c.html#somedata'; // 注意该文件在"a.com"域下
document.body.appendChild(ifrproxy);
}
}
//c.html
//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
这样,利用中间的 c 层就可以用 hash 达到 a 与 b 的交互了。
window.postMessage()
原理
这个方法是 HTML5 的一个新特性,可以用来向其他所有的window对象发送消息。需要注意的是我们必须要保证所有的脚本执行完才发送MessageEvent,如果在函数执行的过程中调用了他,就会让后面的函数超时无法执行。
CORS
这个是今天介绍的重点了,这个是目前几乎最完美的解决方案。也是使用人数最多的方案。
原理
CORS 的全称是 Cross-Origin Resource Sharing,即跨域资源共享。他的原理就是使用自定义的 HTTP 头部,让服务器与浏览器进行沟通,主要是通过设置响应头的 Access-Control-Allow-Origin 来达到目的的。这样,XMLHttpRequest 就能跨域了。是一个W3C标准。
使用方式
设置响应头
字段名 | 是否可选 | 说明 |
---|---|---|
Access-Control-Allow-Origin | 必选 | 表示服务端允许的请求源,*标识任何外域,多个源 , 分隔 |
Access-Control-Allow-Credentials | 可选 | 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true(同时ajax请求中withCredentials要设置位true),即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。 |
Access-Control-Expose-Headers | 可选 | 调用getResponseHeader()方法时候,能从header中获取的参数 |
可能我们会想到,同源策略是浏览器在限制,我们设置响应头有什么用?这时候浏览器还没发送请求呢。浏览器为了实现这个标准,将 CORS 请求分为了简单请求(simple request)和非简单请求(not-so-simple request)。简单请求就直接进行请求,而非简单请求,就会先发送一个 options 类型的嗅探请求,得到回应并检验通过之后才会进行后续的正常请求。
简单请求
(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
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。都以Access-Control-开头。
我们使用axios,来模拟一个请求。在这里使用 cnodejs 提供的接口服务。感谢。
axios.get('https://cnodejs.org/api/v1/topics');
如果打开控制台我们会发现,该请求,虽然跨域,但是却直接请求成功了。就是因为该请求属于简单请求,看一下请求头和响应头。
非简单请求
凡是不同时满足简单请求条件,就属于非简单请求。对于非简单请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。类型为 options。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
我们模拟一个请求
axios.put('https://cnodejs.org/api/v1/topics');
可以看到,有两次请求,第一次请求为 optios 类型,主要是为了检测该请求是否被允许,如果允许,才会继续进行下面的请求。
axios.put('https://baidu.com');
我们再来执行一下这个代码,会发现,只有一个 options
类型的请求,并且控制台抛出异常。原因就是服务端没有允许发起跨域请求。浏览器通过这个 options
类型的请求,得到响应头,并抛出异常。
缺点,古老的浏览器没有实现该标准,所以不支持。由于复杂请求嗅探机制,会让服务器压力增大。
对比
对比以上,我们会发现 CORS 不仅使用方便,支持所有类型请求,具有权限控制,而且浏览器原生支持,我们可以轻易的处理请求异常。利于排查错误。所以我们大多数情况下会首选该方式。在低版本浏览器可以使用jsonp配合其他方式来兼容。
本文由 lscho 创作,采用知识共享署名4.0国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: 05 05, 2018