参考文档,下面有转载【非常好的两篇文章】:
http://www.cnblogs.com/loveis715/p/4592246.html [跨源的各种方法总结]
http://kb.cnblogs.com/page/139725/
[跨域综述]:CORS利用服务器响应头和ajax技术实现跨域消息传递。例外还有其他很多跨域技术,详述如下:
①图像Ping
img的src属性不仅可以用来指定图片url,还可以用来跨域传递消息。不过只能是单向向服务器传递,无法访问服务器响应信息。可以用于跟踪广告浏览量。传给服务器,服务器可以将数据存在第三方媒介,如数据库或文本。
②jsonp
跨域访问一般都要受到同源政策限制,但是Web页面上调用js文件时则不受是否跨域的影响(不仅如此,我们还发现凡是拥有”src”这个属性的标签都拥有跨域的能力,比如<script>、<img>、<iframe>.
jsonp是一种开发人员发挥自己的聪明才智发现的一种跨源消息传递策略。关键在于利用<script>标签加载外部文件。此时加载的外部文件可以是服务器脚本文件,如php文件。把此php文件当做js文件即可。代码如下:
①,本地html文件:URL是:http://localhost/xxx
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>JSONP测试</title></head><body><script> var localHandler = function (data) { alert('我是本地函数,得到服务器返回的数据data后就可以在本地操作了: ' + data); }</script><!--script标签加载外部文件,自动视外部文件为js文件,即使实际上是php文件。所以php文件要输出js代码--><!--可以传递参数过去,callback传递的是本地处理函数名,一般默认此参数名是callback,当然其它的也可以--><!--JSONP只能用GET方法,这算是它的缺点--><script src="http://tong/html/test.php?callback=localHandler"></script></body></html>
②,远程php文件:URL是:http://tong/xxx。 和本地的localhost不属于同源。
<?php //指定此文件是javascript文件 header('content-type:text/javascript'); $callback = $_GET['callback']; //注意,直接把此文件视为js文件即可,直接输出js执行代码,不需要输出<script>标签 $retval = <<<EOF $callback("我是服务器返回的数据"); EOF; echo $retval;
③comet:[这个技术主要并不在于跨域,而在于实时推送]
指的是一种更高级的Ajax技术(也有人称为“服务器推送”)。Ajax是一种从页面向服务器请求数据的技术,而comet则是一种服务器向页面推送数据的技术。有两种实现comet的方式:长轮询和HTTP流。两者的区别是:长轮询适合当条件变化时就推送给客户端消息,而HTTP流适合无条件的一直向客户端推送消息。另外还有一种类似comet的技术,iframe长连接。
※长轮询:(短轮询是客户端向服务器请求数据,服务器立即返回,无论是在有数据)
长轮询的例子:
客户端代码:
function longpolling(url) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function (e) { if (xhr.readyState ==4 && xhr.status == 200){ console.log('DONE'); //一次http连接结束后,立即进行下一次轮询 longpolling('test.php'); } }; xhr.open('get',url,true); xhr.send(null); } longpolling('test.php');
服务器端代码:
<?php set_time_limit(0); $filename = 'a.txt'; $time = time(); while (true) { $mtime = filemtime($filename); /**如果进入了if语句,则会跳出。这次http连接就此结束。此时客户端ajax的xhr.readyState是4. * 如果没有进入if语句,则一直循环,不会响应客户端,所以此时客户端xhr.readyState一直为1. */ if ($mtime > $time) { echo "检测到文件变化!"; break; } clearstatcache();//很关键,否则服务器无法检测到文件变化时间 sleep(1); } /** * clearstatcache()函数的用法: * clearstatcache() 函数清除文件状态缓存。 * clearstatcache() 函数会缓存某些函数的返回信息,以便提供更高的性能。但是有时候,比如在一个脚本中多次检查同一个文件,而该文件在此脚本执行期间有被删除或修改的危险时,你需要清除文件状态缓存,以便获得正确的结果。要做到这一点,就需要使用 clearstatcache() 函数。 * 会进行缓存的函数,即受 clearstatcache() 函数影响的函数: * stat() * lstat() * file_exists() * is_writable() * is_readable() * is_executable() * is_file() * is_dir() * is_link() * filectime() * fileatime() * filemtime() * fileinode() * filegroup() * fileowner() * filesize() * filetype() * fileperms() */ //$i = 0; //while(true){ // echo "Number is $i"; // flush(); // ob_flush(); // sleep(1); // $i++; // break; // if($i==10){ // break; // } //}
※HTTP流
流不同于轮询,因为它在页面的整个生命周期内只使用一个HTTP链接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性的向浏览器发送数据。代码如下:
客户端:
function httpStream(url) { var xhr = new XMLHttpRequest(); var receivedLength = 0; xhr.onreadystatechange = function (e) { var result; if (xhr.readyState == 2) { console.log('已收到服务器响应的标头'); } else if (xhr.readyState == 3) { result = xhr.responseText.substr(receivedLength); receivedLength += result.length; console.log('正在接收服务器响应数据:'); console.log("-------------------"); console.log(" " + result + ";"); console.log("-------------------") } else if (xhr.readyState == 4 && xhr.status == 200) { console.log('服务器响应完毕!!'); console.log(xhr.responseText); } }; xhr.open('get', url, true); xhr.send(null); } httpStream('test.php');
服务器端:
<?php set_time_limit(0); $i = 0; /** * $i < 10时,会一直刷新数据给客户端,此时客户端的xhr.readyState一直为3. * 当$i=10就会结束此次http连接,此时客户端的xhr.readyState为4. * 实际应用中,应该用while(true)永不退出来实现实时推送。 */ while ($i < 10) { echo "Number is $i"; /**flush()和ob_flush()一起用的效果是:每次"冲刷"数据都会传给客户端, * 但要注意: * 第一次冲刷的数据为:Number is 0,然后发给客户端; * 第二次冲刷的数据也包含第一次的,具体为:Number is 0Number is 1,然后发给客户端; * 以此类推。 * 而如果只用其中之一,则效果是:将数据积累完毕之后再一次性传给客户端。 * 而我们的目的是服务器实时推送,所以一次性积累完毕在发送没什么意义,所以要两者一起用。 */ flush(); ob_flush(); sleep(1); $i++; }
※iframe长连接
iframe长连接类似于HTTP流,都是 一个HTTP连接始终保持不关闭。代码如下:
客户端很简单,就一个iframe框架
服务端:(同样是检测文件变化通知)
$filename = 'a.txt'; $time = time(); while (true) { $mtime = filemtime($filename); if ($mtime > $time) { echo "检测到文件变化!!"; //两个flush函数都需要 flush(); ob_flush(); $time = $mtime; } clearstatcache();//很关键,否则服务器无法检测到文件变化时间 sleep(1); }
为了简化Comet,出现了两个新的接口:SSE和WebSocket
④,服务器发送事件SSE(Server-Sent Events):
两篇非常全面的SSE文章:
http://lib.csdn.net/article/html5/36526 [读完应该就理解SSE了]
http://www.52im.net/thread-335-1-1.html 【这个网站非常好】
简略代码如下:
浏览器端代码:
<script type="text/javascript"> var source = new EventSource('test.php'); source.onmessage = function (e) { console.log(JSON.parse(e.data)); console.log(source.readyState); console.log(source.url); console.log("------------------------"); }; source.addEventListener('tong', function (e) { console.log(e.data); console.log('*'); }) </script>
服务器端代码:
<?php header('content-type:text/event-stream'); header('cache-control:no-cache'); /** * 备注: * 1,服务器输出的每个“\n\n”便会触发一次事件监听器,如果带有event字段,触发自定义事件,否则触发onmessage事件, * 2,如果没有while(true),服务器也会源源不断的(不算自己的sleep函数,chromium浏览器是每隔3s推送一次,可以通过retry字段重新设置此时间), * 每次都是一个http连接,可以在控制台的Network选项卡观察到,不停的新增页面。 * 而如果有while(true),则会按照自己的sleep函数指定的间隔源源不断的推送,这时始终是一个未断开的http连接。 * 3,每条消息以\n结束,每次消息以\n\n结束。上一个\n\n和下一个\n\n之间的所有消息都属于同一个事件(自定义或onmessage),下例中的aaa,bbb,ccc都属于自定义事件tong的消息 * 4,每条消息由 字段名 冒号 字段值组成,各个字段的含义如下: *规范中规定了下面这些字段: event 事件类型.如果指定了该字段,则在客户端接收到该条消息时,会在当前的EventSource对象上触发一个事件,事件类型就是该字段的字段值,你可以使用addEventListener()方法在当前EventSource对象上监听任意类型的命名事件, 如果该条消息 没有event字段,则会触发onmessage属性上的事件处理函数. data 消息的数据字段.如果该条消息包含多个data字段,则客户端会用换行符把它们连接成一个字符串来作为字段值. id 事件ID,会成为当前EventSource对象的内部属性"最后一个事件ID"的属性值. retry 一个整数值,指定了重新连接的时间(单位为毫秒),如果该字段值不是整数,则会被忽略. 除了上面规定的字段名,其他所有的字段名都会被忽略. 注: 如果一行文本中不包含冒号,则整行文本会被解析成为字段名,其字段值为空. */ while (1) { $time = date('r'); echo "data:aaa\n"; echo "data:bbb\n"; echo "event:tong\n"; echo "data:ccc\n\n"; $f = <<<EOF data:{"name":"tong","age":11}\n\n EOF; echo $f;//json格式的输出 echo "id:31\n\n"; echo "retry:1000\n\n"; ob_flush(); flush(); sleep(1); }
⑤Web Socket
必须给WebSocket构造函数传入绝对URL。同源策略对Web Socket不适用,因此可以通过它打开到任何站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器(通过握手信息就可以知道请求来自何处).
=============================================
文章转载:
说说JSON和JSONP
前言
由于Sencha Touch 2这种开发模式的特性,基本决定了它原生的数据交互行为几乎只能通过AJAX来实现。
当然了,通过调用强大的PhoneGap插件然后打包,你可以实现100%的Socket通讯和本地数据库功能,又或者通过HTML5的WebSocket也可以实现与服务器的通讯和服务端推功能,但这两种方式都有其局限性,前者需要PhoneGap支持,后者要求用户设备必须支持WebSocket,因此都不能算是ST2的原生解决方案,原生的只有AJAX。
说到AJAX就会不可避免的面临两个问题,第一个是AJAX以何种格式来交换数据?第二个是跨域的需求如何解决?这两个问题目前都有不同的解决方案,比如数据可以用自定义字符串或者用XML来描述,跨域可以通过服务器端代理来解决。
但到目前为止最被推崇或者说首选的方案还是用JSON来传数据,靠JSONP来跨域。而这就是本文将要讲述的内容。
JSON(JavaScript Object Notation)和JSONP(JSON with Padding)虽然只有一个字母的差别,但其实他们根本不是一回事儿:JSON是一种数据交换格式,而JSONP是一种依靠开发人员的聪明才智创造出的一种非官方跨域数据交互协议。我们拿最近比较火的谍战片来打个比方,JSON是地下党们用来书写和交换情报的“暗号”,而JSONP则是把用暗号书写的情报传递给自己同志时使用的接头方式。看到没?一个是描述信息的格式,一个是信息传递双方约定的方法。
既然随便聊聊,那我们就不再采用教条的方式来讲述,而是把关注重心放在帮助开发人员理解是否应当选择使用以及如何使用上。
什么是JSON
前面简单说了一下,JSON是一种基于文本的数据交换方式,或者叫做数据描述格式,你是否该选用他首先肯定要关注它所拥有的优点。
JSON的优点:
1、基于纯文本,跨平台传递极其简单;
2、Javascript原生支持,后台语言几乎全部支持;
3、轻量级数据格式,占用字符数量极少,特别适合互联网传递;
4、可读性较强,虽然比不上XML那么一目了然,但在合理的依次缩进之后还是很容易识别的;
5、容易编写和解析,当然前提是你要知道数据结构;
JSON的缺点当然也有,但在作者看来实在是无关紧要的东西,所以不再单独说明。
JSON的格式或者叫规则:
JSON能够以非常简单的方式来描述数据结构,XML能做的它都能做,因此在跨平台方面两者完全不分伯仲。
1、JSON只有两种数据类型描述符,大括号{}和方括号[],其余英文冒号:是映射符,英文逗号,是分隔符,英文双引号""是定义符。
2、大括号{}用来描述一组“不同类型的无序键值对集合”(每个键值对可以理解为OOP的属性描述),方括号[]用来描述一组“相同类型的有序数据集合”(可对应OOP的数组)。
3、上述两种集合中若有多个子项,则通过英文逗号,进行分隔。
4、键值对以英文冒号:进行分隔,并且建议键名都加上英文双引号”",以便于不同语言的解析。
5、JSON内部常用数据类型无非就是字符串、数字、布尔、日期、null 这么几个,字符串必须用双引号引起来,其余的都不用,日期类型比较特殊,这里就不展开讲述了,只是建议如果客户端没有按日期排序功能需求的话,那么把日期时间直接作为字符串传递就好,可以省去很多麻烦。
JSON实例:
// 描述一个人 var person = { "Name": "Bob", "Age": 32, "Company": "IBM", "Engineer": true } // 获取这个人的信息 var personAge = person.Age; // 描述几个人 var members = [ { "Name": "Bob", "Age": 32, "Company": "IBM", "Engineer": true }, { "Name": "John", "Age": 20, "Company": "Oracle", "Engineer": false }, { "Name": "Henry", "Age": 45, "Company": "Microsoft", "Engineer": false } ] // 读取其中John的公司名称 var johnsCompany = members[1].Company; // 描述一次会议 var conference = { "Conference": "Future Marketing", "Date": "2012-6-1", "Address": "Beijing", "Members": [ { "Name": "Bob", "Age": 32, "Company": "IBM", "Engineer": true }, { "Name": "John", "Age": 20, "Company": "Oracle", "Engineer": false }, { "Name": "Henry", "Age": 45, "Company": "Microsoft", "Engineer": false } ] } // 读取参会者Henry是否工程师 var henryIsAnEngineer = conference.Members[2].Engineer;
关于JSON,就说这么多,更多细节请在开发过程中查阅资料深入学习。
什么是JSONP
先说说JSONP是怎么产生的:
其实网上关于JSONP的讲解有很多,但却千篇一律,而且云里雾里,对于很多刚接触的人来讲理解起来有些困难,小可不才,试着用自己的方式来阐释一下这个问题,看看是否有帮助。
1、一个众所周知的问题,Ajax直接请求普通文件存在跨域无权限访问的问题,甭管你是静态页面、动态网页、web服务、WCF,只要是跨域请求,一律不准;
2、不过我们又发现,Web页面上调用js文件时则不受是否跨域的影响(不仅如此,我们还发现凡是拥有”src”这个属性的标签都拥有跨域的能力,比如<script>、<img>、<iframe>);
3、于是可以判断,当前阶段如果想通过纯web端(ActiveX控件、服务端代理、属于未来的HTML5之Websocket等方式不算)跨域访问数据就只有一种可能,那就是在远程服务器上设法把数据装进js格式的文件里,供客户端调用和进一步处理;
4、恰巧我们已经知道有一种叫做JSON的纯字符数据格式可以简洁的描述复杂数据,更妙的是JSON还被js原生支持,所以在客户端几乎可以随心所欲的处理这种格式的数据;
5、这样子解决方案就呼之欲出了,web客户端通过与调用脚本一模一样的方式,来调用跨域服务器上动态生成的js格式文件(一般以JSON为后缀),显而易见,服务器之所以要动态生成JSON文件,目的就在于把客户端需要的数据装入进去。
6、客户端在对JSON文件调用成功之后,也就获得了自己所需的数据,剩下的就是按照自己需求进行处理和展现了,这种获取远程数据的方式看起来非常像AJAX,但其实并不一样。
7、为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
如果对于callback参数如何使用还有些模糊的话,我们后面会有具体的实例来讲解。
JSONP的客户端具体实现:
不管jQuery也好,ExtJs也罢,又或者是其他支持jsonp的框架,他们幕后所做的工作都是一样的,下面我来循序渐进的说明一下jsonp在客户端的实现:
1、我们知道,哪怕跨域js文件中的代码(当然指符合web脚本安全策略的),web页面也是可以无条件执行的。
远程服务器remoteserver.com根目录下有个remote.js文件代码如下:
alert('我是远程文件');
本地服务器localserver.com下有个jsonp.html页面代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript" src="http://remoteserver.com/remote.js"></script> </head> <body> </body> </html>
毫无疑问,页面将会弹出一个提示窗体,显示跨域调用成功。
2、现在我们在jsonp.html页面定义一个函数,然后在远程remote.js中传入数据进行调用。
jsonp.html页面代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript"> var localHandler = function(data){ alert('我是本地函数,可以被跨域的remote.js文件调用,远程js带来的数据是:' + data.result); }; </script> <script type="text/javascript" src="http://remoteserver.com/remote.js"></script> </head> <body> </body> </html>
remote.js文件代码如下:
localHandler({"result":"我是远程js带来的数据"});
运行之后查看结果,页面成功弹出提示窗口,显示本地函数被跨域的远程js调用成功,并且还接收到了远程js带来的数据。很欣喜,跨域远程获取数据的目的基本实现了,但是又一个问题出现了,我怎么让远程js知道它应该调用的本地函数叫什么名字呢?毕竟是jsonp的服务者都要面对很多服务对象,而这些服务对象各自的本地函数都不相同啊?我们接着往下看。
3、聪明的开发者很容易想到,只要服务端提供的js脚本是动态生成的就行了呗,这样调用者可以传一个参数过去告诉服务端“我想要一段调用XXX函数的js代码,请你返回给我”,于是服务器就可以按照客户端的需求来生成js脚本并响应了。
看jsonp.html页面的代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript"> // 得到航班信息查询结果后的回调函数 var flightHandler = function(data){ alert('你查询的航班结果是:票价 ' + data.price + ' 元,' + '余票 ' + data.tickets + ' 张。'); }; // 提供jsonp服务的url地址(不管是什么类型的地址,最终生成的返回值都是一段javascript代码) var url = "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998&callback=flightHandler"; // 创建script标签,设置其属性 var script = document.createElement('script'); script.setAttribute('src', url); // 把script标签加入head,此时调用开始 document.getElementsByTagName('head')[0].appendChild(script); </script> </head> <body> </body> </html>
这次的代码变化比较大,不再直接把远程js文件写死,而是编码实现动态查询,而这也正是jsonp客户端实现的核心部分,本例中的重点也就在于如何完成jsonp调用的全过程。
我们看到调用的url中传递了一个code参数,告诉服务器我要查的是CA1998次航班的信息,而callback参数则告诉服务器,我的本地回调函数叫做flightHandler,所以请把查询结果传入这个函数中进行调用。
OK,服务器很聪明,这个叫做flightResult.aspx的页面生成了一段这样的代码提供给jsonp.html(服务端的实现这里就不演示了,与你选用的语言无关,说到底就是拼接字符串):
flightHandler({ "code": "CA1998", "price": 1780, "tickets": 5 });
我们看到,传递给flightHandler函数的是一个json,它描述了航班的基本信息。运行一下页面,成功弹出提示窗口,jsonp的执行全过程顺利完成!
4、到这里为止的话,相信你已经能够理解jsonp的客户端实现原理了吧?剩下的就是如何把代码封装一下,以便于与用户界面交互,从而实现多次和重复调用。
什么?你用的是jQuery,想知道jQuery如何实现jsonp调用?好吧,那我就好人做到底,再给你一段jQuery使用jsonp的代码(我们依然沿用上面那个航班信息查询的例子,假定返回jsonp结果不变):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head> <title>Untitled Page</title> <script type="text/javascript" src=jquery.min.js"></script> <script type="text/javascript"> jQuery(document).ready(function(){ $.ajax({ type: "get", async: false, url: "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998", dataType: "jsonp", jsonp: "callback",//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback) jsonpCallback:"flightHandler",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据 success: function(json){ alert('您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。'); }, error: function(){ alert('fail'); } }); }); </script> </head> <body> </body> </html>
是不是有点奇怪?为什么我这次没有写flightHandler这个函数呢?而且竟然也运行成功了!哈哈,这就是jQuery的功劳了,jquery在处理jsonp类型的ajax时(还是忍不住吐槽,虽然jquery也把jsonp归入了ajax,但其实它们真的不是一回事儿),自动帮你生成回调函数并把数据取出来供success属性方法来调用,是不是很爽呀?
好啦,写到这里,我已经无力再写下去,又困又累,得赶紧睡觉。朋友们要是看这不错,觉得有启发,给点个“推荐”呗!由于实在比较简单,所以就不再提供demo源码下载了。
没想到上了博客园的头条推荐。看到大家对这篇文章的认可和评论,还是很开心的,这里针对ajax与jsonp的异同再做一些补充说明:
4月20日下午补充
1、ajax和jsonp这两种技术在调用方式上“看起来”很像,目的也一样,都是请求一个url,然后把服务器返回的数据进行处理,因此jquery和ext等框架都把jsonp作为ajax的一种形式进行了封装;
2、但ajax和jsonp其实本质上是不同的东西。ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加<script>标签来调用服务器提供的js脚本。
3、所以说,其实ajax与jsonp的区别不在于是否跨域,ajax通过服务端代理一样可以实现跨域,jsonp本身也不排斥同域的数据的获取。
4、还有就是,jsonp是一种方式或者说非强制性协议,如同ajax一样,它也不一定非要用json格式来传递数据,如果你愿意,字符串都行,只不过这样不利于用jsonp提供公开服务。
总而言之,jsonp不是ajax的一个特例,哪怕jquery等巨头把jsonp封装进了ajax,也不能改变这一点!
============================================================================================================
文章转载
CORS简介
现在请跟我做:在您的浏览器的地址栏中输入www.yhd.com并敲击回车。在网站内容全部加载完毕后,按F12打开浏览器的调试窗口。当切换到Sources页时,您会发现您当前所看到的一号店的页面是从多个不同的域中得到的:
或许有些读者会感到奇怪:在之前自己 写网页的时候就曾经尝试访问非当前域中的资源,却怎么也不成功,一号店是如何做到的?
当然,这不是一号店的独门绝技,而仅仅是使用了一些跨域访问的技术而已。而在本文中,我们就将对一种跨域访问技术CORS(Cross-Origin Resource Sharing)进行介绍。
为什么要用CORS
在需要做出一个技术决定时,我们常常需要给出适当的理由。就CORS而言,使用它的根本原因就是要完成资源的跨域访问,也就是如何绕过Same-origin Policy。
那么什么是Same-origin Policy呢?简单地说,在一个浏览器中访问的网站不能访问另一个网站中的数据,除非这两个网站具有相同的Origin,也即是拥有相同的协议、主机地址以及端口。一旦这三项数据中有一项不同,那么该资源就将被认为是从不同的Origin得来的,进而不被允许访问。
但是这个限制的确过于严格了:一个大型网站常常拥有一系列子域。在这些域之间交换数据就会受到Same-origin Policy的限制。为了绕过该限制,业界提出了一系列解决该问题的方法,例如更改document.domain属性,跨文档消息,JSONP以及CORS等。这些解决方案各有各的长处,因此我们需要根据需求的不同来对这些方案进行选择。
可以说更改document.domain属性的方法是最为直接快速的的方法,也较为常见。通过将从不同域中得到的脚本的document.domain属性设置为同一个值,就可以使得这些脚本之间可以相互交互。例如从“http://blog.ambergarden.com”得到的网页可以通过执行如下的脚本改变其document.domain属性中记录的所属域:
1 document.domain = ‘ambergarden.com’;
那么接下来,该脚本就可以访问ambergarden.com中的数据了。
这种方法也有其自身的劣势,那就是软件开发人员不可以随便设置document.domain属性的值,至少在一些浏览器上是如此的。
跨文档消息则是通过向Window实例发送消息来完成的。在使用时,软件开发人员需要通过调用一个Window的postMessage()函数来向该Window实例发送消息。此时Window实例内部的onmessage事件将被触发,进而使得该事件的消息处理函数被调用。但是在接收到消息的时候,消息处理函数首先需要判断消息来源的合法性,以避免恶意用户通过发送消息的方式来非法执行代码。
JSONP则是通过在文档中嵌入一个<script>标记来从另一个域中返回数据。例如在页面中添加一个如下的<script>标记:
1 <script src="http://blog.ambergarden.com/someData?callback=some_func"/>
该<script>标记会向http://blog.ambergarden.com/someData发送一个GET请求。在数据返回到客户端后,some_func()函数将会被调用。当然,这种方法拥有一个显著的缺点,那就是只支持GET操作。
就如您刚刚看到的一样,上面所列出的各个方法各自有各自的缺点及局限性。而相较于这些方法,CORS则没有那么多工作需要去做,也没有那么多限制。因此在本文中,我们将主要对CORS进行讲解。
CORS运行流程
现在我们就来看一个通过CORS来进行跨域访问的简单示例。假设ambergarden.com想从一个公有数据平台public-data.com中返回一些数据,那么在页面逻辑中,其可以通过下面的代码向public-data.com发送数据请求:
1 function retrieveData() { 2 var request = new XMLHttpRequest(); 3 request.open('GET', 'http://public-data.com/someData', true); 4 request.onreadystatechange = handler; 5 request.send(); 6 }
在运行这段代码的之后,浏览器会向服务发送如下的请求:
1 GET /someData/ HTTP/1.1 2 Host: public-data.com 3 ...... 4 Referer: http://ambergarden.com/somePage.html 5 Origin: http://ambergarden.com
而一个支持CORS协议的服务可能会给出下面的响应:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Content-Type: application/xml 4 ...... 5 6 [Payload Here]
这里有一个值得注意的响应头:Access-Control-Allow-Origin。该响应头用来记录可以访问该资源的域。在接收到服务端响应后,浏览器将会查看响应中是否包含Access-Control-Allow-Origin响应头。如果该响应头存在,那么浏览器会分析该响应头中所标示的内容。如果其包含了当前页面所在的域,那么浏览器就将知道这是一个被允许的跨域访问,从而不再根据Same-origin Policy来限制用户对该数据的访问。
从整个访问数据的流程来看,用户所使用的跨域访问数据的脚本实际上和普通的访问同一个域中数据的脚本并没有什么不同。而不同的,仅仅是在响应中多了一个Access-Control-Allow-Origin响应头。
是不是很简单?实际上我们展示的仅仅是最为简单的Simple Request的执行流程。而CORS则将导致跨域访问的请求分为三种:Simple Request,Preflighted Request以及Requests with Credential。
如果一个请求没有包含任何自定义请求头,而且它所使用HTTP动词是GET,HEAD或POST之一,那么它就是一个Simple Request。但是在使用POST作为请求的动词时,该请求的Content-Type需要是application/x-www-form-urlencoded,multipart/form-data或text/plain之一。
如果一个请求包含了任何自定义请求头,或者它所使用的HTTP动词是GET,HEAD或POST之外的任何一个动词,那么它就是一个Preflighted Request。如果POST请求的Content-Type并不是application/x-www-form-urlencoded,multipart/form-data或text/plain之一,那么其也是Preflighted Request。
一般情况下,一个跨域请求不会包含当前页面的用户凭证。一旦一个跨域请求包含了当前页面的用户凭证,那么其就属于Requests with Credential。
前面我们已经看过浏览器对Simple Request是如何进行处理的。那么接下来我们就来看看Preflight Request是如何执行的。相较于Simple Request,Preflight Request的运行流程则略为复杂一些。
假设现在我们要向公有数据平台public-data.com写入一些数据,那么我们就需要发送一个POST请求:
1 function sendData() { 2 var request = new XMLHttpRequest(), 3 payload = ......; 4 request.open('POST', 'http://public-data.com/someData', true); 5 request.setRequestHeader('X-CUSTOM-HEADER', 'custom_header_value'); 6 request.onreadystatechange = handler; 7 request.send(payload); 8 }
在执行了该段代码之后,浏览器首先发出的请求将如下所示:
1 OPTIONS /someData/ HTTP/1.1 2 Host: public-data.com 3 ...... 4 Origin: http://ambergarden.com 5 Access-Control-Request-Method: POST 6 Access-Control-Request-Headers: X-CUSTOM-HEADER
可以看到,我们首先发送的并不是POST请求,而是OPTION请求。该请求还通过Access-Control-Request-Method以及Access-Control-Request-Headers标示了请求类型以及请求中所包含的自定义HTTP Header。实际上,它相当于向服务端询问访问资源的权限:“您好,我想向你这里发送数据,你看可以吗?”。而在真正访问资源前发送一个请求进行探测也是该请求被称为是Preflight Request的原因。
在服务端看到该OPTIONS请求后,其将分析该请求中的内容并返回一个响应,以通知浏览器是否允许向它发送数据:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Access-Control-Allow-Methods: POST, GET, OPTIONS 4 Access-Control-Allow-Headers: X-CUSTOM_HEADER 5 Access-Control-Max-Age: 1728000 6 ......
浏览器分析该响应并了解到其被允许向服务端发送数据以后,其才会向服务端发送真正的POST请求:
1 POST /someData/ HTTP/1.1 2 Host: public-data.com 3 X-CUSTOM-HEADER: custom_header_value 4 ...... 5 6 [Payload Here]
而服务端则会接收并处理该请求:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Content-Type: application/xml 4 ...... 5 6 [Payload Here]
最后一种请求Requests with Credential的运行流程则和前两种请求类似。只不过在发送请求的时候,我们需要将用户凭证包含在请求中:
1 function retrieveData() { 2 var request = new XMLHttpRequest(); 3 request.open('GET', 'http://public-data.com/someData', true); 4 request.withCredentials = true; 5 request.onreadystatechange = handler; 6 request.send(); 7 }
而在服务端的响应中,其将拥有一个额外的Access-Control-Allow-Credentials响应头:
1 HTTP/1.1 200 OK 2 Access-Control-Allow-Origin: http://ambergarden.com 3 Content-Type: application/xml 4 ...... 5 6 [Payload Here]
集成对CORS的支持
从上面的示例中已经能够看到,在使用CORS来访问数据的时候,客户端不需要更改任何数据访问逻辑。所有的一切工作都是在服务端及浏览器之间自动完成的。因此如果希望为一个系统集成CORS支持的时候,我们需要做的工作主要集中在服务端。
当然,集成工作实际上十分简单:在你的web.xml中添加一个Filter(或利用已有的Filter)并根据传入的请求首先判断其是哪一种CORS请求。在得知了请求的类型后,我们就可以决定到底以哪种方式响应用户了。这里的逻辑较为简单,因此我就不再赘述了。