你的位置:首页 > Java教程

[Java教程]同源策略和跨域请求研究


一、同源策略

假设有一个需求,需要向另外的网站请求数据,例如抓取谷歌搜索的结果。然后写这么一个请求,搜索内容为hello:

var url = "https://www.google.com.hk/?gws_rd=cr,ssl#newwindow=1&safe=strict&q=hello";$.ajax({  url: url,   sucess: function(data){    document.write(data);  }});

或者用原生的更直观:

    var req = new "GET", url);    req.send();

执行后,浏览器会报错:

大意是说localhost域名无法向google.com域名请求数据。

因为同源策略的限制,不同域名、协议(http、https)或者端口无法直接进行ajax请求。 同源策略只针对于浏览器端,浏览器一旦检测到请求的结果的域名不一致后,会堵塞请求结果。这里注意,跨域请求是可以发去的,但是请求响应response被浏览器堵塞了

写了一个程序做验证——用node开了个服务,监听在9000端口,然后在8000端口打开一个页面,再向9000端口的服务发请求:

    url = "http://server.com:9000";    $.ajax({      method: "POST",      url: url,      data: {        account: "yin"      },      success: function(data){        document.write(data);      }    });

服务将收到的请求数据打印出来:

服务收到了请求,并正常返回数据,但是返回的数据被浏览器干掉了,即使是返回码也无法得到了。所以说同源策略是限制了不同源的读,但不限制不同源的写。那么我们的问题来了,为什么不直接限制写呢,只限制读有什么好处呢?在回答这个问题之前,先要了解同源策略的作用。

假设我打开了A网银http://Abank.com,已经通过了登陆验证,然后再打开了另外一个黑网站http://evil.com,这个网站刚好是抓使用Abank.com的肉鸡。在evil.com的代码里会向Abank.com发请求,例如转账请求,将余额转到自己的账户。但是由于同源策略的限制,使得这种做法无法成功。这个怎么解释呢?

因为evil.com无法获取你在Abank.com的信息,包括验证身份的信息——通常是按照一定规则生成的无法猜到的随机token字符串。token可能放在cookie里面,从evil.com向Abank发请求时,是不会带上Abank的cookie的,同时也不会带上evil.com的cookie,虽然cookie是和域名绑定的。由于没有正确的token值,导致无法通过服务的身份验证。

为验证没带cookie,在上面的例子,localhost向server.com请求数据,服务将收到的cookie打印出来是undefined:

然而localhost已经设置了cookie:

server.com也有设置cookie:

回到上面的问题,为什么不限制写呢?那是因为如果连请求也不出去,那在源头上就限制死了,网站之间就无法共享资源了。另外,限制读即浏览器拦截请求结果,一般情况下就够了,一方面如果访问的是黑网站,那么网站无法跟据请求结果继续下一步的操作,如不断地猜测密码,另一方面如果访问的是白网站,block掉请求结果,应该是考虑到了请求结果可能会使得页面重定向,或者是给网页添加一个恶意的iframe之类的。

有什么办法可以绕过同源策略?有一个办法就是CSRF攻击

二、CSRF攻击

如上面的例子,由于同源策略的限制,跨域的ajax请求不会带cookie,然而script/iframe/img等标签却是支持跨域的,所以在请求的时候是会带上cookie的。还是上面的例子,如果登陆了Abank.com,那么cookie里面就有了tocken,同时又打开了另外一个标签页访问了evil.com,这个网页里面有一个iframe:

<iframe src="http://Abank.com/app/transferFunds?amount=1500&destinationAccount=... >

这个iframe的src是一个Abank.com的转账的请求,如果Abank.com的转账请求没有第二重加密措施的话,那么请求转账就成功了!

第二个例子是路由器的配置,假设我在网上找到了一个路由器配置教程的网站。这个网站里面偷偷地加一个img标签:

<img src=”http://192.168.1.1/admin/config/outsideInterface?nexthop=123.45.67.89” alt=”pwned” height=”1” width=”1”/>

其中192.168.1.1是很多路由器的配置地址。这个1像素的图片没加载出来被忽略了,但是它的请求却发出去了。这个请求给路由器添加了一个vpn代理,指向黑客的代理服务器。如果路由器也是把登陆验证放在cookie里面,那么这个设置vpn的请求很可能就成功了,以后的连接路由器的每个请求都会先经过黑客的服务。

到这里,很明显一个防CSRF攻击的策略就是将token添加到请求的参数里面,也就是说每个需要验证身份的请求都要显式地带上token值。详见:Cross-Site Request Forgery Guide: Learn All About CSRF Attacks and CSRF Protection

 

用script引用的外域的资源一方面可以像上面一样当作一个跨域的请求,另外一方面虽然资源是不可见的,但是script里面定义的全局对象是可用的,如引用jQuery的CDN,定义的一个全局对象jQuery。所以根据这个特性,在某些条件下可以获得到script返回的需要登陆才能得到的数据,有兴趣的可参见:Plain text considered harmful: A cross-domain exploit

跨域攻击可以采取一些措施进行规避,但是跨域更多的还是一些实际的正常应用。

三、跨域请求

有时候在自己的网站需要一些去别人的网站请求数据,这个时候就需要跨域正常请求。方法有很多:

1. 跨域资源共享(CORS)

很多天气、IP地址查询的网站就采用了这样的方法,允许其它网站对其请求数据,例如IP location,可以在自己网站的js里面向它发一个get请求:

    var url = "https://ipinfo.io/54.169.237.109/json?token=iplocation.net";    document.cookie = "version=1;";    $.ajax({ url: url })

它就会返回ip地址信息,同时不会被浏览器拦截:

观察response的头部,可以发现添加了一个字段:

Access-Control-Allow-Origin就是所谓的资源共享了,它的值*表示允许任意网站向这个接口请求数据,也可以设置成指定的域名,如:

response.writeHead(200, { "Access-Control-Allow-Origin": "http://yoursite.com"});

在node.js服务里面添加这个头,那么只有http://yoursite.com能够正常的进行跨域请求。更多地,还可以指定请求的方式、时间等,详见:HTTP访问控制(CORS)

2. JSONP

另外一个常用的办法是使用jsonp,这个方法的原理是客户端告诉服务一个回调函数的名称,服务在返回的scritp里面调用这个回调函数,同时传进客户端需要的数据,这样返回的代码就在浏览器执行了。

例如8000端口要向9000端品请求数据,在8000端口的页面文件定义一个回调函数writeDate,将writeDate写在script的src的参数里,这个script标签向9000端口发出请求:

  <script>    function writeDate(_date){      document.write(_date);    }  </script>  <script src="http://192.168.0.103:9000/getDate?callback=writeDate"></script>

服务端返回一个脚本,在这个脚本里面执行writeDate函数:

function getDate(response, callback){  response.writeHead(200, {"Content-Type": "text/javascript"});  var data = "2016-2-19";  response.end(callback + "('" + data + "')");}

浏览器就执行了这个script片段:

这样就实现了跨域的效果。jQuery的ajax里的jsonp的类型,就是用了这样的办法,只是jQuery将它封装好了,使用起来形式跟普通的get/post一样,但是原理是不一样的。

JSONP和CORS相比较,缺点是只支持get类型,无法支持post等其它类型,必须完全信任提供服务的第三方,优点是兼容性较好。

3. 子域跨父域

子域跨父域是支持的,但是需要显式将子域的域名改成父域的,例如mail.mysite.com要请求mysite.com的数据,那么在mail.mysite.com脚本里需要执行:

document.domain = "mysite.com";

4. iframe跨父窗口

如果iframe与父窗口也有同源策略的限制,父域无法直接读取不同源的iframe的DOM内容以及监听事件,但是iframe可以调用父窗口提供的api。iframe通过window.parent得到父窗口的window对象,然后父窗口定义一个全局对象供iframe调用。

例如在页面通过iframe的方式嵌入一个youtobe的视频,如果需要手动播放视频、监听iframe的播放事件,页面需要引入youtobe的视频播放控制api,在这个js文件里面定义了一个全局对象YT:

if (!window['YT']) {var YT = {loading: 0,loaded: 0};}

而在视频iframe的脚本里通过window.parent获取得到父窗口即自己网站的页面:

sr = new Cq(window.parent, d, b)

自已网站的页面也是在这个YT对象自定义一些东西,如添加播放事件监听:

new YT.Player('video', { events:{ 'onStateChange': function(data){//do sth. } } });

5. window.postMessage

在上面第(4)点,父窗口无法向不同源的iframe传递东西,通过window.postMessage可以做到,父窗口向iframe传递一个消息,而iframe监听消息事件。

例如在8000端口的页面嵌入了一个9000端口的iframe:

<iframe src="http://server.com:9000"></iframe>

然后9000端口post一个message:

window.onload = function(){      window.frames[0].postMessage("hello, this is from http://localhost:8000/", "http://server.com:9000/");    }

postMessage执行的上下文必须是接收信息的window,传递两个参数,第一个是数据,第二个是目标窗口。

同时,iframe即9000端口的页面监听message事件:

  window.addEventListener("message", receiveMessage);  function receiveMessage(event){    var origin = event.origin || event.originalEvent.origin;     //身份验证    if (origin !== "http://localhost:8000"){       return;    }    console.log("receiveMessage: " + event.data);   }

这样子iframe就可收到父窗口的信息了:

同理iframe也可以向父窗口发送消息:

window.parent.postMessage("hello, this is from http://server.com:9000", "http://localhost:8000");

父窗口收到:

window.postMessage也适用于通过window.open打开的子窗口,方法类似。

补充一点,如果iframe与父窗口是同源的,则父窗口可以直接获取到iframe的内容,这个方法常用于无刷新上传文件。