秋招复习计划-JavaScript知识点7

  |  

前言

跨域、同源策略及跨域实现方式与原理


34



同源策略

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全共嗯,如果缺少了同源策略,浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础质上的,浏览器只是针对同源策略的一种实现。

同源策略的核心在于他认为来自任何站点装载的内容是不安全的。当被浏览器运行时,被装载的内容制备允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。

同源是指:域名、协议、端口相同

同源策略又分为以下两种:

  • DOM同源策略:禁止对不同源页面的DOM进行操作。主要是针对iframe跨域的情况,不同域名的iframe是限制互相访问的
  • XMLHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。

为什么要有跨域限制

如果浏览器么有同源策略,会存在什么样的问题

没有DOM同源策略

没有DOM同源策略,也就是说不同域的iframe之间可以相互访问。那么我们写一个网页,这个网页里面只有一个iframe标签,这个标签嵌套了一个银行网站。我们将iframe设置为同页面等高宽,那么这看起来跟正常的银行网站没有任何区别。但是因为没有了DOM同源策略,所以我们可以通过外网页去获取iframe中DOM节点,就能获得用户输入的账号密码

没有XMLHttpRequest同源策略

没有XMLHttpRequest同源策略的话,就能发动CSRF(跨站请求伪造)

用户登陆了自己的银行页面,页面为了优化用户体验,向用户的cookie中添加用户标识,短时间内用户不需要重复登陆

黑客提前破解了银行页面转账的请求参数方式,并编写了一个AJAX请求在恶意页面中。用户访问了这个恶意页面。

由于没有同源策略,恶意页面成功向银行页面发起了HTTP请求,请求默认带上了银行账号信息的cookie

银行页面后端验证cookie,发现的确是之前登陆的用户,于是转账请求被成功处理了

由此可见浏览器同源策略存在的重要性。

跨域的实现方式及其原理

虽然浏览器同源策略对用户安全提供了重要的保障,但是有时候我们不得不突破同源策略,比如说当你的网站前端页面在一个服务器上,而后端和数据库在另一台服务器上,那我们不得不跨域去访问后端服务器。跨域又很多方式,下面一一介绍

CORS(跨域资源共享)

CORS(Cross-origin resource sharing,跨域资源共享)是一个W3C标准,定义了在必须访问跨域资源时,浏览器与服务器该如何沟通。CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求是否响应成功或失败

CORS需要浏览器和服务器同时支持。目前浏览器都支持该功能,IE10之前不支持

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者而言,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨院,就会自动添加一些附加的头部信息。因此实现CORS通信的关键在于服务器,只要服务器实现了CORS接口,就可跨域通信了。

CORS相关的头部:

  • Access-Control-Allow-Origin:该头部由服务器返回,用来明确指定哪些客户端的域名允许访问该资源。他的值可以是一个完整的域名或者*(表示允许任意域名),但是如果你需要可不短传递验证信息(例如cookie)到头部,这个值必须是完整的域名
  • Access-Control-Allow-Credentials:这个头部信息只会在服务器支持通过cookies传递验证信息的返回数据里。他的值只有一个就是true。跨站点带验证信息时,服务器必须要设置这个值,服务器才能获取到用户的cookie。
  • Access-Control-Allow-Header:这个头部体的值是一个用逗号分割的列表,在返回Preflight 请求时出现,表示下一次的简单请求中哪些头部可以使用,例如在下一次的简单请求中要使用自定的x-autherntication-token头部,那么服务器在上一次返回Preflight 请求的Access-Control-Allow-Header头部中就要加上那个值
  • Access-Control-Allow-Methods:一个逗号分割的列表,表明服务器支持的请求类型
  • Access-Control-Max-Age:这个响应首部表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存多久。单位为秒,为-1表示禁用缓存
  • Origin:这个头部信息,属于请求数据的一部分。这个值表明这个请求是从浏览器打开的哪个域名下发出的。出于安全原因,浏览器不允许你修改这个值。

浏览器对CORS请求的处理

浏览器将CORS请求分为两类:简单请求和非简单请求。

简单请求的要求:

  • 请求方式只能是HEAD、GET、POST
  • HTTP的头部信息不能超过这些字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(只限于application/x-www-form-urlencoded、multipart/form-data、text/plain三个值)

不满足这些条件的都是非简单请求

浏览器对这两种不同的请求采用不同的处理方式

对于简单请求:

  • 在请求中添加额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口)
  • 如果服务器认为这个请求是可以接受的,就在返回的响应Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发 * )
  • 如果响应没有这个头部或者有这个头部但信息不匹配,浏览器就不会处理响应。正常情况下会处理。注意,一般情况下请求和响应都不包含cookie信息
  • 如果需要包含cookie信息,ajax请求就需要设置xhr对象的withCredentials属性为true,服务器需要设置响应头部 Access-Control-Allow-Credentials: true

对于非简单请求:

浏览器在发送真正的请求之前,会先发送一个Preflight请求给服务器,这种请求使用OPTIONS方法,发送下列头部:

  • Origin:与简单请求相同
  • Access-Control-Request-Method:真正的请求将使用的的方法。
  • Access-Control-Request-Headers: 真正的请求的(可选)自定义的头部信息,多个头部以逗号分隔。

发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通

Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Max-Age

一旦服务器通过Preflight请求允许该请求之后,以后每次浏览器符合要求的正常CORS请求,就都跟简单请求一样了

CORS的优点:

  • CORS通信与同源的AJAX通信没有差别,代码相同,易于维护

  • 支持所有类型的HTTP请求

CORS的缺点:

  • 存在兼容性问题,不过现在看来也不是什么大问题,只是IE10一下的浏览器不支持
  • 每一次发送非简单请求会多一次请求

JSONP跨域

由于script标签不受浏览器同源策略的影响,允许跨域引用资源。因此可以通过动态创建script标签,然后使用src属性进行跨域,这也就是JSONP跨域的基本原理。下面来看一个示例

1
2
3
4
5
6
7
function handleResponse(data) {
console.log(data);
}
let body = document.getElementsByTagName('body')[0];
let script = document.createElement('script');
script.src = "http://www.doubler.cn/json?callback=handleResponse";
body.appendChild(script);

分析这个示例,我们创建一个函数用来处理数据,然后获得body标签,并动态创建一个script标签,通过src去请求一个网址。后端获得请求的参数,然后返回调用对应函数的代码

1
2
3
handleResponse({
"name": "doubler"
})

浏览器在接受到响应后,返回的JS代码会在script标签中被执行,后端返回的数据就被带到了JS中使用了。由此我们完成了一次跨域请求

JSONP的优点:

  • 使用简便,没有兼容问题,是目前最常用的跨域方式之一

JSONP的缺点:

  • 只支持GET请求的跨域
  • 由于是从其他域中加载代码执行,因此如果其它域不安全,可能会在响应中夹带一些恶意代码
  • 要确定JSONP请求是否失败并不容易,虽然HTML5给script标签添加了onerror事件处理程序,但是并不是所有浏览器都支持

图像Ping跨域

由于img标签不受浏览器同源策略的影响,允许跨域引用。因此可以通过img标签的src属性进行跨域

1
2
3
4
5
let img = new Image();
img.onload = img.onerror = function() {
console.log("Done");
}
img.src = 'http://www.doubler.cn/test?name=doubler';

图像Ping的有点:

  • 用于实现跟踪用户点击页面或动态广告曝光次数有较大的又是

图像Ping的缺点:

  • 只支持GET请求
  • 只能从浏览器向服务器发送单向通信,因为浏览器不能访问服务器的响应文本

服务器代理

浏览器受同源策略的限制,但是浏览器并不存在这样的限制,所以可以通过前端请求后端接口,后端实现跨域的资源访问,这主要通过后端实现,所以这里不作过多讲述

document.domain跨域

对于主域名相同,而子域名不同的情况,可以使用document.domain来进行跨域,这主要应用于iframe跨域。

例如有一个页面他的地址是http://www.doubler.cn/a.html,页面中嵌入一个iframe,他的src属性是http://doubler.cn/b.html,显然页面和iframe中的页面不再同一个域,所以我们无法通过页面中的js代码来操作iframe中dom节点。但是我们可以设置页面的document.domain,将他设置为iframe的src的值,这样它们就同域了。

但是需要注意的是document.domain能设置的值是有限制的,他只能设置为自身或更高级的父域。及对于域为a.b.doubler.cn的页面,它的document.domain能设置为a.b.doubler.cnb.doubler.cndoubler.cn,但是他就不能设置为a.b.c.doubler.cn。而且它们的主域必须相同,及必须是以doubler.cn为主域

例如,在页面http://www.doubler.cn/a.html中设置document.domain

1
2
3
4
5
6
7
<iframe src="http://doubler.cn/b.html" id="myIframe" onload="test()"/>
<script>
document.domain = 'doubler.cn'; //设置主域
function test() {
console.log('done');
}
</script>

同时我们还要在http://doubler.cn/b.html中设置document.domain,虽然他的值就是doubler.cn但是还是要显示设置。设置完成后,我们就能在子域页面中使用JS来访问到父域中dom

window.name跨域

window对象有一个name属性,该属性有一个特征,即在一个窗口的生命周期内,窗口载入的所有页面(不管是同域的还是不同域的)都共享一个window.name,每个页面对window.name属性都拥有读写权限,且window.name是持久存在于一个窗口载入的所有页面额,并不会因为新页面的载入而进行重置

http://www.doubler.cn/a.html页面的代码

1
2
3
4
5
6
7
8
9
<iframe src="http://doubler.cn/b.html" id="myIframe" onload="init()" />
<script>
function init() {
let iframe = document.getElementById("myIframe");
let data = null;
data = JSON.prase(iframe.contentWindow.name);
iframe.src = 'http://www.doubler.cn/c.html';
}
</script>

http://doubler.cn/b.html的代码

1
2
3
4
5
6
<script>
let data = {
value: 'test'
}
window.name = JSON.stringify(data);
</script>

分析这个跨域的例子,子域页面中有一个iframe标签,src为父域的一个页面,当它被加载时触发onload事件。回调函数获得iframe标签对象,从父域页面中获得window.name的值并将其反序列化。然后重置iframe的src属性,加载我们真正需要的同域页面。

再来看父域页面,它里面将一个对象序列化以后存入window.name中,这样当其它不同域的页面加载这个页面后就能从window.name中获得需要的值。

window.name默认值是空字符串,他的值也只能是字符串,所以对于对象,需要先经过序列化。

location.hash跨域

location.hash方式跨域,是子框架具有修改父框架src的hash值,通过这个属性进行传递数据。且修改hash值,页面不会刷新。但是缺陷就是传递的数据的字节有限。

例如页面http://www.doubler.cn/a.html

1
2
3
4
5
6
7
<iframe src="http://doubler.cn/b.html" id="myIframe" onload="test()" style="display:none" />
</script>
function test() {
let data = window.location.hash;
console.log(data);
}
</script>

页面http://doubler.cn/b.html

1
2
3
<script>
parent.location.hash = "value"; // top.location.hash = "value"
</script>

这里使用parent对象或top对象设置父窗口或最顶层窗口的hash值

postMessage跨域

window.postMessage(message, targetOrigin)方法是HTML5新引入的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源。这个方法可能就是以后解决dom跨域的主流方法了

调用postMessage方法的window对象是要接收数据的那个window对象,该方法的第一个参数message为要发送的消息,第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,如果不想限定与可以使用通配符

1
2
3
4
5
6
7
8
<iframe src="http://doubler.cn/b.html" id="myIframe" onload="test()" />
<script>
function test() {
let iframe = document.getElementById('myIframe');
let win = iframe.contentWindow;
win.postMessage('test', "*");
}
</script>
1
2
3
4
5
6
<script>
window.onmessage = function(e) {
e = e || event;
console.log(e.data);
}
</script>
文章目录
  1. 1. 同源策略
  2. 2. 为什么要有跨域限制
  3. 3. 跨域的实现方式及其原理
    1. 3.1. CORS(跨域资源共享)
    2. 3.2. JSONP跨域
    3. 3.3. 图像Ping跨域
    4. 3.4. 服务器代理
    5. 3.5. document.domain跨域
    6. 3.6. window.name跨域
    7. 3.7. location.hash跨域
    8. 3.8. postMessage跨域
|