浏览器的同源策略与跨域问题的解决方案

同源策略(Same-Origin-Policy,SOP)

同源策略是一种约定,是浏览器最核心也最基本的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。比如a.com下的js脚本采用ajax读取b.com里面的文件数据是会报错的。如果缺少了同源策略,则浏览器的正常功能都可能会受到影响。可以说Web是构建在同源策略基础上的,浏览器只是针对同源策略的一种实现。

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

为什么要有同源策略

浏览器主要是从两个方面去做同源策略的,一是针对接口的请求,二是针对DOM的查询。

接口请求上的同源策略

有一个小小的东西叫cookie大家都知道,一般是用来处理登陆等场景,目的是让服务端知道请求是谁发出的。如果你请求了接口进行登陆,服务端验证通过之后就会在响应头(Response Headers)加入Set-Cookie属性,然后下次再发请求的时候,浏览器就会自动将cookie附加在HTTP请求头(Request Headers)的cookie属性中,服务端就能知道这个用户已经登陆过了。

知道了cookie的工作原理后,我们来看没有同源策略的场景:

1.你准备去剁手,于是打开了买买买网站,登陆,选了一些东西到购物车。

2.你还没选够东西呢,突然弹出来一个链接,你一点,跳到了骗骗骗网站。

3.骗骗骗网站吸引住你了,你饶有兴趣地浏览着,却不知道这个网站在暗地里做了坏事:它向买买买网站发起了请求!因为你登陆和浏览买买买网站的时候已经产生了cookie在浏览器中,那骗骗骗网站因为没有同源策略的限制,就可以偷偷拿到这个cookie,就相当于登陆了你买买买网站的账号,然后为所欲为,偷偷地给你买一堆面膜。

这就是传说中的CSRF(Cross-site Request Rorgery,跨站请求伪造)攻击。

DOM查询上的同源策略

有一天你收到一条短信,说是你的银行账号有风险,要你赶紧点进www.yinghang.com更改密码。你吓尿了,卡里有三十多块钱呢,于是你果断点进去,看到还是熟悉的银行登陆界面,果断地输入你的账号密码。着急的你却没有看清楚,其实平时访问的是www.yinhang.com。那么问题来了,这个钓鱼网站究竟会做什么呢?

首先,它会在网页中内嵌一个iframe,iframe中是真的www.yinhang.com。

<iframe name="yinhang" src="www.yinhang.com"></iframe>

然后,因为没有同源策略限制,它就能拿到www.yinhang.com网站的DOM。

window.frames['yinhang'].document.getElementById('你输入账号密码的input').val();

通过DOM去获取输入的账号密码,然后取走你卡里的三十多块钱,让你痛不欲生。

综上所述,同源策略是能有效规避一些风险的。但不是说有了同源策略就一定安全的,只是说同源策略是浏览器的一种最基本的安全机制,毕竟能提高一点攻击成本。事实上,没有刺不穿的盾,只有攻击的成本和攻击成功后获得收益的正比。

源(Origin)

我们知道,URL(统一资源定位符)是由协议、域名、端口和路径组成的。通常理解的源,就是指的是URL中的协议、域名和端口的组合。

浏览器的同源策略与跨域问题的解决方案

既然URL是描述资源的,那么源也是描述资源的一个部分。

这样理解,静静是个来自山西的女孩子,芳芳是个来自广东的女孩子。描述静静的时候,就会说她是来自山西的静静;描述芳芳的时候,就会说她是来自广东的芳芳。这个来自哪里,就是源。

同源(Same-Origin)

如果两个URL的协议、域名和端口相同,则称它们是同源的。

莉莉也是来自广东的女孩子,这时就可以说她和来自广东的芳芳是同源的。

跨域(Cross-Origin)

与同源相反的,只要协议、域名和端口三个组成中有任何一个不同,就视为不同源。

从一个源(预)去请求不同源(域)的资源,就称为跨域。

浏览器中有一些不受同源限制的标签,比如说<script>、<img>、<iframe>、<link>这些包含src属性的标签可以加载跨域的资源。但是浏览器限制了JavaScript的权限使其不能读或写这些标签内加载的内容。

如何解开JavaScript的限制去读、写这些标签内加载的内容,实际上就是我们要解决的跨域问题。

跨域的解决方案

跨域主要有几个解决方案:降域、postMessage、JSONP、CORS和反向代理等。

同源策略限制下接口请求的跨域解决方案:JSONP、CORS和反向代理等。

JSONP

前面提到,在HTML标签里一些标签,比如说<script>、<img>这样带src属性的获取资源的标签,是没有跨域限制的。利用这一点,我们就可以干点坏事,呸,好事。

现在有个a.com/jsonp.html想要得到b.com/main.js中的数据,就可以在a.com的jsonp.html里面创建一个回调函数callback1,动态地添加<script>元素,然后向服务器发送请求,在请求地址后面加上查询字符串,最后通过callback参数指定回调函数的名字。

这时的请求地址就是b.com/main.ja?callback=callback1。在main.js中调用这个回调函数callback1,并且以JSON数据的形式作为参数传递,完成回调。我们来看看代码:

/* a.com/jsonp.html中的代码 */

// 创建script标签
function addScriptTag(src) { 
    var script = document.createElement('script'); 
    script.setAttribute("type", "text/javascript"); 
    script.src = src; 
    document.body.appendChild(script);
}

// 页面加载完成后再执行
window.onload = function () { 
    addScriptTag("http://b.com/main.js?callback=foo");
}

// 回调函数
function foo(data) { 
    console.log(data.name + "好可爱");
};
/* b.com/main.js中的代码 */

foo({name:"静静"}); // 调用回调函数,把要的东西传给它

这样,就完成了跨域的参数传递。

JSONP的注意事项:

1.JSONP和JSON看起来很相似,但是其实他们之间没有半毛钱关系。

2.使用这种方式的话,只要是个网站都能拿到b.com里的数据,存在安全性问题,需要网站双方商议基础token的身份验证。

3.JSONP只能是GET的方式,不支持POST等其它方式。

4.JSONP可能在回调函数中被注入恶意代码,篡改页面内容。可以采用字符串过滤的方法规避此问题。

5.jQuery或AngularJs中有提供封装好的jsonp方法。

CORS(CrossOrigin Resource Sharing, 跨源资源共享)

CORS是一个W3C标准,看名字就知道这时处理跨域问题的标准做法。CORS有两种请求,简单请求(Simple Request)和非简单请求(Not-so-simple Request)。

简单请求

同时满足以下两个条件,就属于简单请求:

1.请求方法必须是HEAD、GET、POST三种方法之一。

2.HTTP的头信息不能多于Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(只限application/x-www-form-urlencoded、mltipart/form-data、text/plain三个值)这些字段。

非简单请求

非简单请求会发出一次预检测请求,返回码是204,预检测通过之后才会发真正的请求,这才返回200。可以通过前端发请求的时候增加一个额外的headers来触发非简单请求。

CORS的实现方式比较复杂,要用另外的篇幅去叙述,这里就不做叙述了(懒,哈哈哈哈)。

反向代理

试想一下,如果我们请求的时候还是用前端的域名,但是当提交到后台的时候,有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?当前很火的Nginx就是一个能解决跨域问题的反向代理应用。

Nginx的配置

server{
    # 监听9009端口
    listen 9009;
    # 域名是localhost
    server_name localhost;
    # 凡是localhost:9009/api这个样子的,都转发到真正的服务端地址localhost:9888上去
    location ^~ /api {
        proxy_pass http://localhost:9888;
    }    
}

前端的请求

http://localhost:9009/api/dosomething

前端请求的时候直接使用前端这边的域名localhost:9009即可,这样就是同源了,不会有跨域的问题。Nginx监听到凡是localhost:9009/api这样的,都转发到真正的服务端地址localhost:9888上。

Nginx转发请求的方式似乎很方便,但是这种使用是要看场景的。比如说如果后端接口是一个公共服务的API,去获取天气什么的,前端调用的时候总不能让运维去配Nginx吧。如果浏览器兼容性没问题的话(IE10或以上),CORS才是更通用的做法。

同源策略限制下DOM查询的跨域解决方案:降域、postMessage等。

降域(Descending Domain)

同源策略认为域和子域属于不同的域。比如说:

child1.parent.com与parent.com不同源

child1.parent.com与child2.parent.com不同源

grandson1.child1.parent与child1.parent.com不同源

可以通过设置document.domain =  'parent.com'让浏览器认为他们都属于同一个源。想要实现以上任意两个页面之间的通信,两个页面都必须这样设置。

降域的特点与注意事项:

1.降域主要针对于Cookie和当前页面下的iframe。

2.降域只能在父域名和子域名之间使用,即只适合在同一主域名下使用。且将grandson1.child1.parent.com的document.domain设置为parent.com之后,不能再设置成child1.parent.com。

3.降域需要有降的空间,*域名无法降域。

4.降域是要双向设置document.domain的,不仅当前页面要设置,iframe内嵌的页面也要设置。

5.降域存在安全性问题,当一个站点被攻击后,另一个站点也会引起安全漏洞。

postMessage

window.postMessage()是HTML5的一个接口,专注实现不同窗口不同页面的跨域通讯。

具体使用这里就不做叙述了(还是懒,嘿嘿)。

"别说了,我想静静。"

"静静是谁?"