如何理解web安全——同源策略

什么是同源策略?

    浏览器同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行狡猾。

    同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。只有同一个源的脚本赋予dom、读写cookie、session、ajax等操作的权限。url由协议、域名、端口和路径组成、如果两个url的协议、域名和端口相同,则这两个url是同源的。限制来源不用源的“document”,对当前的“document”读取或设置某些属性。在不受同源策略限制,带有“src”属性的标签加载是,实际上是由游览器发起一次GET请求,不同于XMLHTTPRequest,它们通过src属性加载的资源。但游览器限制了JavaScript的权限,使其不能读,写其中返回的内容。

以http://abc.com/为例

url  是否跨域  原因
http://abc.com/other  否  协议、端口、主机相同
https://abc.com  是  不同的协议(https)
http://abc.com:81  是  端口不用(81)
http://news.abc.com/  是  不同的主机(news)
http://www.abc.com/  是  域名不用,顶级域名与www子域名不是一个概念
postMessage(HTML5)

语法

otherWindow.postMessage(message, targetOrigin, [transfer]);

// otherWindow 发送窗口,比如iframe的contentWindow
// message 发送信息
// targetOrigin 定哪些窗口能接收到消息事件 "*" 表示无限制
// transfer [可选] 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

跨源网络访问

    同源策略控制了不用源之间的交互、交互通常分为三类

  • 允许进行跨域写操作(cross-origin writes)。例如链接(links)、重定向以及表单提交
  • 允许跨域资源嵌入(cross-origin embedding)
  • 不允许跨域读操作(cross-origin reads)。但常可以通过内嵌资源来巧妙的进行读取访问

可能的跨域资源嵌入实例

  •   <script src=""></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。
  • <link rel="stylesheet" href=""/>标签嵌入css。由于css的松散的语法规则,css的跨域需要一个正确的content-type消息头。不用的浏览器有不同的限制。
  • <img>嵌入图片
  • <video>和<audio>嵌入多媒体资源
  • <object>,<embed>和<applet>的插件
  • @font-face引入的字体。一些浏览器允许跨域字体(cross-origin fonts),一些需要同源字体
  • <frame>和<iframe>载入的任何资源。站点可以使用x-Frame-Options消息头来阻止这种形式的跨域交互

同源策略的限制

  • Cookie、LocalStorage和IndexDB无法读取
  • DOM无法获得
  • AJAX请求不能发送 

如何突破同源策略

   受同源策略的限制,只有同源网站的 Cookie才能共享。

   1.cookie

    cookie只有同源的网站才能获取,但是如果两个网页的一级域名相同,只有二级域名不同,可以设置相同的document.domain,两个网页就可以共享cookie了

    域名的等级划分:

  1. 顶级域名:.com
  2. 一级域名:baidu.com
  3. 二级域名:tieba.baidu.com

  例如,a网页是http://abc.com/a.html,b网页是http://abc.com/b.html,我们可以设置document,.domain = 'abc.com',a与b可以共享cookie

   2.iframe

    如果两个网页不用源,就无法拿到对方的DOM。典型的例子是iframe窗口和用window.open方法打开的窗口,它们与父窗口无法通信。所以对于完全不同源的网站,目前可以使用以下几种方法突破同源问题:

  1. 片段标识符(fragement identifier)
  2. window.name
  3. 跨文档通信API(window.postMessage)
  • 片段标识符
      片段标识符指的是URL中#后面的内容,比如http://ab.com/search.html#search中的#search,如果只是改变片段标识符,页面不会重新刷新。
      父窗口可以把信息写入子窗口的片段标识符:
var src=originURL + '# '+data
document.getElementById('myIframe').src = src

     子窗口通过监听hashchange时间得到通知:

window.onhashchange = function(){
  console.log(window.location.hash)
}
  • window.name

     浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。

  • window.postMessage

     HTML5为了解决跨窗口通讯问题引入了一个新的API:跨文档通信API。window.postMessage方法允许跨窗口通讯,不论这两个窗口是否同源。

    例子:父窗口为http://a.com,子窗口为http://b.com

//父窗口向子窗口发送消息
var popup = window.open('http://b.com','title');
popup.postMessage('hello world','http:/b.com')

    postMessage()方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议+域名+端口”。也可以设为*,表示不限制域名,向所有的窗口发送

    同样,子窗口向父窗口发送消息可以这样写:

window.opener.postMessage('hi,this is a.com','http://a.com')

    父窗口和子窗口都可以通过message事件,监听对方的消息

window.addEventListener('message', function(e) {
    console.log(e.data)
},false)

    message事件和event对象有以下三个属性:

  1. event.source发送消息的窗口
  2. event.origin消息发送的网址
  3. event.data消息内容

例子,子窗口通过event.source属性引用父窗口,然后发送消息。

window.addEventListener('message',receiveMessage);
function receiveMessage(event){
	event.source.postMessage('nice to see you!','*')
}

如何我们将发送的消息改为localStorage,则可以互相读取localStorage.

3.AJAX

同样ajax请求也会受到同源策略的影响,除了使用代理服务器外,还有一个方法可以实现跨域;

  • jsonp
  • WebSocket
  • CORS

1.jsonp

    jsonp由两部分组成,回调函数和数据。其基本思路是:动态插入script标签,向服务器请求json数据,返回的数据将在回调函数中获取。

function addScript(src){
	var script = document.createElement('script');
	script.setAttribute('type','text/javascript');
	script.src = src;
	document.body.appendChild(script);
}

//回调函数
function foo(data){
	console.log('You public IP address is:'+data.ip)
};

window.onload = function(){
	addScript('http://example.com/ip?callback=foo')
}

   上面的代码通过动态添加script元素,向服务器example.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字。

2.WebSocket

    WebSocket不用于http,它提供一种双向通讯的功能,即客户端可以向服务器请求数据,同时服务器也可以向客户端发送数据。而http只能是单向的。

    同事WebSocket使用ws:\//(非加密)和wss:\//(加密)作为协议前缀,该协议不实行同源策略,只要服务器支持,就可以通过它进行跨域通信。

   要创建WebSocket,先实例化一个WebSocket对象并传入要连接的URL:

var socket = new WebSocket('ws://www.example.com/server.php')

    实例化WebSocket对象之后,浏览器会马上尝试建立连接。与XHR类似,WebSocket也有一系列表示当前状态的readyState属性,如下:

  1. WebSocket.OPENING:正在建立连接
  2. WebSocket.OPEN:已经建立连接
  3. WebSocket.CLOSEING:正在关闭连接
  4. WebSocket.CLOSE:已经关闭连接

    WebSocke没有readyStatechange事件:不过它有其他的事件,下面会介绍。

要关闭WebSocket连接,可以调用close()方法:

scoket.close()

    WebSocket连接之后,就可以发送和接收数据,要发送数据可以调用send()方法,并传入字符串,例如:

var socket = new WebSocket('ws://www.example.com/server.php')
socket.send('hello world')

    因为WebSocket只能发送纯文本数据,所以对于复杂的数据类型我们应先将其序列化转化json字符串

var message = {
  name:'sillywa'
}
socket.send(JSON.stringify(message))

    同样服务器必须先解析再读取数据。

    当服务器向客户端发来消息时,WebSocket对象就会触发message事件。这个message事件与其它传递消息的协议类似,也就是把返回的数据保存在event.data的属性中。

socket.message = function(event){
    console.log(message)
}

    与通过send()发送到服务器的数据一样,event.data中返回的数据也是字符串。

    WebSocket对象还有其它三个事件,在连接生命周期的不同阶段触发。

  • open:在成功建立连接时触发
  • error:在发生错误时触发,连接不能持续
  • close:在连接关闭时触发

    WebSocket对象不支持DOM2级事件侦听器,因此必须使用DOM0级语法分别定义每个事件处理程序。

var socket = new WebSocket('ws://www.example.com/server.php')
socket.onopen = function(){
     console.log('connection start')
}
socket.onerror = function(){
    console.log('connection error')
}
socket.onclose = function(event){
    console.log(event)
}

   这三个事件中只有close的event对象有额外的信息。这个事件的对象有三个额外的属性:wasClean、code、reason.其中wasClean是一个布尔值,表示连接是否已经明确地关闭,close是服务器返回的数据状态码:reason是一个字符串,包含服务器发回的消息。

3.CORS

   CORS是一个W3C标准,全程是"跨域资源共享"(Cross-origin resource sharing)

   它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

   相比于jsonp只能发送get请求,CORS允许发送任何类型的请求。但CORS要求浏览器和服务器同时支持。目前所有浏览器都支持,IE需要IE10以上。

    整个CORS通讯过程中都是浏览器自动完成,不需要用户的参与。CORS通讯和同源的AJAX请求没有区别。浏览器一旦发现AJAX请求跨域,就会自动添加一些头部信息,有时候还会多出一次附加请求。

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

只要同时满足一下两个条件就是简单请求,否则就是非简单请求:

(1)请求方法是下列方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP头信息不超出一下几个字段:

  • Accept
  • Aceept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded/multipart/form-data/text/plain

    对于简单请求,浏览器会自动在头部信息里增加一个Origin字段,用来表示请求来于哪个源,服务器根据这个值决定是否同意此次请求。如果origin不在请求范围内,服务器返回一个正常的http回应。这个回应的头信息中没有Access-Control-Allow-Origin字段,浏览器发现没有这个字段之后就会抛出一个错误。如果origin在请求范围内,服务器返回的响应会多出几个头信息字段,其中一个是Access-Control-Allow-Origin,它的值要么是origin的值,要么是*,表示允许任何域名的请求。

    对于非简单请求,它会在正式通信之前,增加一次http查询请求,成为“预检”请求(preflight)。通常是一个OPION请求。这个请求先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪http动词和头信息字段。只有得到肯定答案,浏览器才会发出正常的XMLHTTPRequest请求,否则报错。

如果大家想要更详细的了解CORS,可以参考以下文章。

参考文章:

阮一峰《浏览器同源政策及其规避方法

阮一峰《跨域资源共享 CORS 详解

参考书籍:

《javascript高级程序设计》






猜你喜欢

转载自blog.csdn.net/maggie_live/article/details/80005177