在跨域安全性方面,有多个地方会有限制,主要是XMLHttpRequest对象的跨域限制和iFrame的跨域限制,下面我们分别来看一下。
Ajax跨域(CORS)
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
以PHP服务端为例
如果一个请求允许http://www.example.com的域请求的话,可以在PHP中这么写:
header("Access-Control-Allow-Origin:http://www.example.com");
如果有多个允许跨域访问的地址,可以添加多条或使用“,”号进行分隔,如果希望所有来源的地址都可以访问当前的页面,可以如下:
header("Access-Control-Allow-Origin:*");
同时,也可以控制请求的方法,如下是允许GET和POST两种请求方法:
header("Access-Control-Allow-Method:POST,GET");
IE中,要跨域需要使用到XDomainRequest的对象,我们这里就不展开来说了,而在其它浏览器中,使用XMLHttpRequest对象即可,并没有不一样的地方。
一个需要注意的地方
最近在公司开发的过程中,遇见的情况是PHP已经添加了header("Access-Control-Allow-Origin:*");代码,但是客户端请求时,仍然报错,最终查出的bug是,包含的其它PHP中,也添加了相同的代码,导致服务端返回时,在Chrome的Network页面中,发现有两条一样的头部数据,去掉一条即可。
CORS跨域之前的跨域方法
在CORS标准出现之前,XHR对象是不能跨域请求的,不过开发人员凭借自己的聪明才智,绕过了XHR对象,创造了另外几种可以跨域请求的方式,我们下面开简单的看看。
图像Ping
前端里面图片元素没有跨域的限制,所以我们可以通过模拟请求一个图片的方法,向服务端发送数据,服务端对应的页面可以返回一个简单的图片数据或者什么数据都不返回(客户端得到204状态)。
1 var img = new Image(); 2 img.onload = img.onerror = function () { 3 console.log("Done"); 4 } 5 img.src = "http://www.example.com/test.php?name=LiLei&age=28";
缺点是可以得到请求成功和失败的回调但是得不到服务端返回的数据,除非将数据放入图片返回。
统计在线广告浏览量等不需要返回数据的情况下通常使用该方式。
JSONP
即JSON with padding的简写,简单来说:就是通过添加一个script标签,通过创建该标签的src地址来传递参数,而服务端返回JS代码的内容,JS代码回调一个页面中已经存在的JS方法,同时将需要给到客户端的信息作为参数传递即可。这样就可以绕过XHR对象实现跨域请求并得到服务端返回的数据。
1 function handleResponse (response) { 2 console.log("name: " + response.name + ", age: " + response.age); 3 } 4 5 var script = document.createElement("script"); 6 script.src = "http://www.example.com/test.php?name=LiLei&age=28"; 7 document.body.insertBefore(script, document.body.firstChild);
当前代码中的handleResponse方法即服务端会回调的方法。
handleResponse({"name":"Han Meimei", "age":27});
服务端返回上面的文本后,由于是添加的script脚本,所以会调用到handleResponse方法并得到服务端的数据。
JSONP缺点
- 访问的其他域如果不安全,可能会返回一些有害的JS代码到当前页面进行执行;
- 难以确定请求失败的响应,需要用户自己实现一个超时计时器。
iFrame跨域
JavaScript出于安全方面的考虑,不允许iFrame跨域访问和调用其他页面的对象。
试想一下,如果我们做了一个钓鱼网站,使用一个iFrame引入了XX银行的首页,把自己伪装成该银行首页,此时如果我们可以跨域调用和修改XX银行的所有数据,也可以通过修改和注入JS代码,后果就是:如果有人登录了我们的假网站,我们就可以通过给这个iFrame添加我们自己的JS代码来轻松获得这个人的帐号和密码信息。
所以,iFrame不允许跨域访问是基于安全的考虑,也是必要的一个安全限制。
我们看下,具体的跨域限制:
- http://www.a.com/a.js http://www.a.com/b.js 同一域名下 允许
- http://www.a.com/lab/a.js http://www.a.com/script/b.js 同一域名下不同文件夹 允许
- http://www.a.com:8000/a.js http://www.a.com/b.js 同一域名,不同端口 不允许
- http://www.a.com/a.js https://www.a.com/b.js 同一域名,不同协议 不允许
- http://www.a.com/a.js http://70.32.92.74/b.js 域名和域名对应ip 不允许
- http://www.a.com/a.js http://script.a.com/b.js 主域相同,子域不同 不允许
- http://www.a.com/a.js http://a.com/b.js 同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)
- http://www.cnblogs.com/a.js http://www.a.com/b.js 不同域名 不允许
但是不可否认的是,在某些情况下,我们还是希望不同域的页面之间可以相互通信(注意这里不一定要相互可以调用修改)。下面我们来看看如何实现跨域的消息通信。
document.domain
通过修改多个框架domain属性为同样的值,可以突破跨域的限制,使多个页面之间可以互相访问到。
但是domain属性的修改有下面两个限制。
只能修改子域为主域,不能修改为其它域
域example.com嵌入了域p2p.example.com的页面,通过修改p2p.example.com的domain即可实现双方互相访问和修改,如同没有跨域一样,但是要注意不能改为其它域:
1 document.domain = "example.com"; // 成功 2 document.domain = "baidu.com"; // 报错
不能将主域修改为子域
如下,位于p2p.example.com的页面:
1 document.domain = "example.com"; // 成功 2 document.domain = "p2p.example.com"; // 报错
因为已经设置为主域了,再次设置会子域会报错。
使用postMessage
跨文档消息传递(cross-document messaging),简称XDM,在H5中该功能可以用来向iFrame嵌套的页面和嵌套自己的页面相互发送消息,也可以向当前页面弹出的窗口相互传递消息。
通过XDM我们可以安全的实现页面之间的跨域消息传递。
XDM得核心方法是postMessage,postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
postMessage(data,origin)方法接受两个参数:
- data:要传递的数据,html5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化,在低版本IE中引用json2.js可以实现类似效果。
- origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage()方法只会将message传递给指定窗口,当然如果愿意也可以建参数设置为"*",这样可以传递给任意窗口。
而在发送了消息之后,符合条件(iFrame嵌套页面或弹出窗口,且符合postMessage的origin参数的域)的其他页面的window对象会收到该消息,作为“message”事件抛出该消息,对应的event对象有如下主要属性:
- data:顾名思义,是传递来的message;
- source:发送消息的窗口对象;
- origin:发送消息窗口的源(协议+主机+端口号)。
下面我们看一个例子:
A.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title>A</title> 6 <body> 7 <p>A</p> 8 <input type="button" value="发送消息到frameB并得到frameB的回应" onclick="sendMsg()"> 9 <iframe id="frameB" src="./B.html" style=" 90%;"></iframe> 10 <script type="text/javascript"> 11 window.addEventListener("message", function (event) { 12 console.log("A.html接收到消息:" + event.data); 13 }); 14 15 function sendMsg () { 16 var frameB = document.getElementById("frameB"); 17 frameB.contentWindow.postMessage("Hello I am Li Lei", "*"); 18 } 19 </script> 20 </body> 21 </html>
B.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title>B</title> 6 </head> 7 <body bgcolor="#ff0000"> 8 <p>B</p> 9 <script type="text/javascript"> 10 window.addEventListener("message", function (event) { 11 console.log("B.html接收到消息:" + event.data); 12 //向发送消息的window回送消息 13 event.source.postMessage("Hi, I am Han Meimei!", "*"); 14 }); 15 </script> 16 </body> 17 </html>
需要注意,window对象发送的消息只有自身可以接收到message消息。
crossOrigin属性
服务端需要配置各种类型资源的跨域范围权限,如Apache服务器可以如下配置:
1 <IfModule mod_setenvif.c> 2 <IfModule mod_headers.c> 3 <FilesMatch ".(cur|gif|ico|jpe?g|png|svgz?|webp)$"> 4 SetEnvIf Origin ":" IS_CORS 5 Header set Access-Control-Allow-Origin "*" env=IS_CORS 6 </FilesMatch> 7 </IfModule> 8 </IfModule>
Node.js如下配置:
1 app.all('*',function (req, res, next) { 2 res.header('Access-Control-Allow-Origin', '*'); 3 res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild'); 4 res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); 5 if (req.method == 'OPTIONS') { 6 res.send(200); /让options请求快速返回/ 7 } else { 8 next(); 9 } 10 });
crossOrigin属性的值
该枚举属性指定在加载相关图片时是否必须使用CORS。
- 当不设置该属性时,资源将会不使用CORS加载(即不发送Origin:HTTP头),这将阻止其在元素中进行使用。若设置了非法的值,则视为使用anonymous。
- "anonymous":会发起一个跨域请求(即包含Origin: HTTP头)。但不会发送任何认证信息(即不发送cookie, X.509证书和HTTP基本认证信息)。如果服务器没有给出源站凭证(不设置Access-Control-Allow-Origin: HTTP头),这张图片就会被污染并限制使用。
- "use-credentials":会发起一个带有认证信息(发送cookie,X.509证书和HTTP基本认证信息)的跨域请求(即包含Origin:HTTP头)。如果服务器没有给出源站凭证(不设置Access-Control-Allow-Origin: HTTP头),这张图片就会被污染并限制使用。
script标签
当我们引入一个不同源的js代码时,不设置crossOrigin属性,在执行上是没有问题的,报错在console面板也可以看到堆栈信息,但是在当前页面使用window.onerror来抓取错误时,只能得到一个Script error的信息,没有更多的信息了。
当我们为script标签添加crossorigin="anonymous"属性后,如果服务端没有对跨域进行设置,会报如下错误:
Access to Script at 'http://127.0.0.1:3000/test.js' from origin 'http://192.168.2.34' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://192.168.2.34' is therefore not allowed access.
服务端进行允许跨域设定后,window.onerror可以抓取到详细的信息了。
img标签
我们在请求其它域下的图片时,可以显示出来,但是当调用toBlob(),toDataURL()和getImageData()等方法时会抛出安全错误,这是由于浏览器的安全策略导致的。
我们先看看下面的代码:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <canvas id='canvas' width='500px' height='500px'></canvas> 9 10 <script> 11 var image = document.createElement("img"); 12 // image.crossOrigin = "anonymous"; 13 image.onload = function() { 14 drawImg(image); 15 }; 16 image.onerror = function() { 17 console.log("load image error!"); 18 }; 19 image.src = "http://127.0.0.1:3000/images/img.png"; 20 21 function drawImg(img) { 22 var canvas = document.getElementById('canvas'); 23 var context = canvas.getContext("2d"); 24 context.drawImage(img, 0, 0, 100, 100); 25 26 var base64 = canvas.toDataURL('image/png'); 27 console.log(base64); 28 } 29 </script> 30 </body> 31 </html>
图片不在当前域,可以绘制到canvas里显示,但是调用toDataURL时会出现跨域的报错,解决方法就是去掉注释,为img标签添加crossOrigin="anonymous"的属性。
添加后,也需要服务端进行允许跨域设定才行哦,刷新后可以得到canvas中的图片编码信息,服务端不设定跨域会得到onerror的事件并且打印跨域异常信息。