1.什么是同源安全策略
同源策略(Same Origin Policy)是一个很重要的安全理念,是客户端脚本的重要的安全度量标准,其目的是防止某个文档或脚本从多个不同源进行装载。根据这个策略,a.com域名下的JavaScript无法跨域操作b.com域名下的对象。比如,baidu.com域名下的页面中包含的JavaScript代码,不能访问google.com域名下的页面内容。JavaScript必须严格遵循浏览器的同源策略,包括Ajax(事实上,Ajax也是由JavaScript组成)。通过XMLHttpRequest对象实现的Ajax请求,不能向不同的域提交,比如,在abc.test.com下的页面,不能向def.test.com提交Ajax请求。这里的"同源"指的是同协议、同域名和同端口。表1中列举的例子能更具体地说明这一点。
表1 示例
URL 1 |
URL 2 |
是否允许通信 |
备 注 |
http://www.a.com/a.js |
http://www.a.com/b.js |
是 |
同域名 |
http://www.a.com/a.js |
http://www.a.com:8080/b.js |
否 |
同域名不同端口 |
http://www.a.com/a.js |
https://www.a.com/b.js |
否 |
同域名不同协议 |
http://www.a.com/a.js |
http://127.0.0.1/b.js |
否 |
域名与其IP均不同 |
http://www.a.com/a.js |
http://test.a.com/b.js |
否 |
主域名和子域名 |
http://www.a.com/a.js |
http://www.b.com/b.js |
否 |
不同域名 |
从第二行开始,网站之间的通信会受到同源策略影响,如http://www.a.com/a.js和http://www.a.com:8080/b.js的端口不一致,所以之间不能通信;http://www.a.com/a.js和https://www.a.com/b.js分别使用了HTTP和HTTPS协议,HTTP、HTTPS、FTP等是属于不同的协议,所以也不能通信;而http://www.a.com/a.js和http://test.a.com/b.js虽然是相同顶级域名,但二级域名不同,所以也是不行的。由此可见,同源策略认为来自其他任何站点的装载内容都是不安全的,如图所示。
这个限制十分重要。假设攻击者利用Iframe把真正的银行登录页面嵌到他的页面上,当用户使用真实的用户名、密码登录时,该页面就可以通过JavaScript读取到用户表单中的内容,这样用户名和密码信息就被泄漏了。而运用了同源安全策略后,用户就能确保自己正在查看的页面确实来自于正在浏览的域。然而,受到同源策略的影响,跨域资源共享就会受到制约。当进行一些比较深入的前端编程的时候,不可避免地需要进行跨域操作,这时候同源策略就显得过于苛刻。于是,开发者就想出各种各样的跨域方法,比如使用JSONP、Iframe、Flash、CORS、服务器中转等。虽然跨域技术能带来更多的功能,但是功能的开放也意味着安全的风险。
2.跨域的方案
2.1 JSONP
2.1.1 JSONP介绍
JSONP(JSON with Padding)是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)。由于同源策略的限制,XmlHttpRequest只允许请求当前源(域名、协议、端口)的资源,为了实现跨域请求,可以通过script标签实现跨域请求,然后在服务端输出JSON数据并执行回调函数,从而解决了跨域的数据请求。
2.1.2 如何使用JSONP
原生JS实现:
或
2.1.3 JSONP原理
首先在客户端注册一个callback, 然后把callback的名字传给服务器。此时,服务器先生成 json 数据。然后以 javascript 语法的方式,生成一个function , function 名字就是传递上来的参数 jsonp.最后将 json 数据直接以入参的方式,放置到 function 中,这样就生成了一段 js 语法的文档,返回给客户端。客户端浏览器,解析script标签,并执行返回的 javascript 文档,此时数据作为参数,传入到了客户端预先定义好的 callback 函数里.(动态执行回调函数)
2.1.4 JSONP 缺点
没有关于 JSONP 调用的错误处理。如果动态脚本插入有效,就执行调用;如果无效,就静默失败。失败是没有任何提示的。例如,不能从服务器捕捉到 404 错误,也不能取消或重新开始请求。不过,等待一段时间还没有响应的话,就不用理它了。(未来的 jQuery 版本可能有终止 JSONP 请求的特性)。JSONP 的另一个主要缺陷是被不信任的服务使用时会很危险。因为 JSONP 服务返回打包在函数调用中的 JSON 响应,而函数调用是由浏览器执行的,这使宿主 Web 应用程序更容易受到各类攻击。如果打算使用 JSONP 服务,了解它能造成的威胁非常重要。
2.2 CORS
2.2.1 CORS介绍
跨域资源共享(CORS )是一种网络浏览器的技术规范,它为Web服务器定义了一种方式,允许网页从不同的域访问其资源。而这种访问是被同源策略所禁止的。CORS系统定义了一种浏览器和服务器交互的方式来确定是否允许跨域请求。 它是一个妥协,有更大的灵活性,但比起简单地允许所有这些的要求来说更加安全。而W3C的官方文档目前还是工作草案,但是正在朝着W3C推荐的方向前进。简言之,CORS就是为了让AJAX可以实现可控的跨域访问而生的。
2.2.2 CORS原理
CORS定义一种跨域访问的机制,可以让AJAX实现跨域访问。CORS 允许一个域上的网络应用向另一个域提交跨域 AJAX 请求。实现此功能非常简单,只需由服务器发送一个响应标头即可。
2.2.3如何使用CORS
利用 CORS,http://www.test2.com 只需添加一个标头,就可以允许来自 http://www.test1.com 的请求,下图是我在PHP中的 hander() 设置,“*”号表示允许任何域向我们的服务端提交请求:
2.3 服务器中转
一个简单的办法就是在本域的服务器上,增加一个转发层,将浏览器上过来的请求接收后,通过服务器将这个请求转发到对应的WebService上,然后把返回结果再取回后,送回js的请求页面。一般而言这个是解决跨域访问最安全与最具兼容性的办法。
2.4 iframe跨域页面嵌套iframe是比较常见的,比如QQ相关业务页面的登录框一般都是iframe的。使用ifrmae跨域要满足一个基本条件,父页面和子页面都是自己可以控制的,如果随便把iframe指向一个其他网站,想通过跨域手段操作它基本上是不可能的。
2.4.1 document.domain
document.domain
是比较常用的跨域方法。实现最简单但只能用于同一个主域下不同子域之间的跨域请求,比如 foo.com 和 img.foo.com 之间,img1.foo.com 和 img2.foo.com 之间。只要把两个页面的document.domain
都指向主域就可以了,比如document.domain='foo.com';
。设置好后父页面和子页面就可以像同一个域下两个页面之间访问了。父页面通ifr.contentWindow
就可以访问子页面的window,子页面通过parent.window
或parent
访问父页面的window,接下来可以进一步获取dom和js。
<iframe id="ifr" src="http://img.foo.com/b.html">iframe>
<script>
document.domain = 'foo.com';
function aa(str) {
console.log(str);
}
window.onload = function () {
document.querySelector('#ifr').contentWindow.bb('aaa');
}
script>
<script>
document.domain = 'foo.com';
function bb(str) {
console.log(str);
}
parent.aa('bbb');
script>
2.4.1 window.name
只要不关闭浏览器,window.name
可以在不同页面加载后依然保持。尝试在浏览器打开百度baidu.com
,然后在控制台输入window.name='aaa';
回车,接着在地址栏输入qq.com
转到腾讯首页,打开控制台输入window.name
查看它的值,可以看到输出了"aaa"
。例如子页面bar.com/b.html向父页面foo.com/a.html传数据。
<iframe id="ifr" src="http://bar.com/b.html">iframe>
<script>
function callback(data) {
console.log(data)
}
script>
<input id="txt" type="text">
<input type="button" value="发送" onclick="send();">
<script>
var proxyA = 'http://foo.com/aa.html'; // foo.com下代理页面
var proxyB = 'http://bar.com/bb.html'; // bar.com下代理空页面
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
document.body.appendChild(ifr);
function send() {
ifr.src = proxyB;
}
ifr.onload = function() {
ifr.contentWindow.name = document.querySelector('#txt').value;
ifr.src = proxyA;
}
script>
top.callback(window.name)
2.4.3 location.hash
较常用,把传递的数据依附在url上,例如获取子页面bar.com/b.html的高度及其他数据
<iframe id="ifr" src="http://bar.com/b.html">iframe>
<script>
function callback(data) {
console.log(data)
}
script>
window.onload = function() {
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
var height = document.documentElement.scrollHeight;
var data = '{"h":'+ height+', "json": {"a":1,"b":2}}';
ifr.src = 'http://foo.com/aa.html#' + data;
document.body.appendChild(ifr);
}
var data = JSON.parse(location.hash.substr(1));
top.document.getElementById('ifr').style.height = data.h + 'px';
top.callback(data);
2.4.4 postMessage
HTML5新增方法,现在浏览器及IE8+支持,简单易用高大上。
.postMessage(message, targetOrigin)
参数说明
message: 是要发送的消息,类型为 String、Object (IE8、9 不支持)
targetOrigin: 是限定消息接收范围,不限制请使用 '*'
'message', function(e)
回调函数第一个参数接收 Event 对象,有三个常用属性:
data: 消息
origin: 消息来源地址
source: 源 DOMWindow 对象
一个简单的父页面foo.com/a.html
和子页面 bar.com/b.html
建立通信
<iframe id="ifr" src="http://bar.com/b.html">iframe>
<script>
window.onload = function () {
var ifr = document.querySelector('#ifr');
ifr.contentWindow.postMessage({a: 1}, '*');
}
window.addEventListener('message', function(e) {
console.log('bar say: '+e.data);
}, false);
script>
window.addEventListener('message', function(e){
console.log('foo say: ' + e.data.a);
e.source.postMessage('get', '*');
}, false)
2.5 Flash 跨域
flash有自己的一套安全策略,服务器可以通过crossdomain.xml文件来声明能被哪些域的SWF文件访问,SWF也可以通过API来确定自身能被哪些域的SWF加载。当跨域访问资源时,例如从域www.a.com请求域www.b.com上的数据,我们可以借助flash来发送HTTP请求。首先,修改域www.b.com上的crossdomain.xml(一般存放在根目录,如果没有需要手动创建) ,把www.a.com加入到白名单。其次,通过Flash URLLoader发送HTTP请求,最后,通过Flash API把响应结果传递给JavaScript。Flash URLLoader是一种很普遍的跨域解决方案,不过需要支持iOS的话,这个方案就无能为力了。