同源策源
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A 网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取 A 网站的 Cookie,会发生什么?很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,“同源政策”是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
随着互联网的发展,“同源政策”越来越严格。目前,如果非同源,共有三种行为受到限制。
1、Cookie、LocalStorage 和 IndexedDB 无法读取
2、DOM 无法获得
3、AJAX 请求无效(可以发送,但浏览器会拒绝接受响应)
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。
产生跨域的原因
浏览器限制、跨域、xhr请求,这三个条件同时发生才会产生跨域问题。
解决跨域问题
CORS(Cross-Origin Resource Sharing)是目前主流的解决跨域的方案。除此之外,script、image、iframe的src都不受同源策略的影响,可以借助这一特点,实现跨域。
CORS
简单请求
浏览器将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、multipart/form-data、text/plain)。
凡是不同时满足上面两个条件,就属于非简单请求。
CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,需要给它附加一个额外的origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。
浏览器如果发现跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。
Accept:*/*
Accept-Encoding:gzip, deflate, br
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
Connection:keep-alive
Content-Length:0
Host:www.webhuochai.com
Origin:http://127.0.0.1
Referer:http://127.0.0.1/cors.html
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36
上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin
头部中回发相同的源信息(如果是公共资源,可以回发" *" )。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror
回调函数捕获。但是,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
[注意]请求和响应都不包含cookie信息
原生支持
标准浏览器都通过XMLHttpRequest
对象实现了对CORS的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可。
<input id="btn" type="button" value="跨域请求">
<div id="result"></div>
<script>
btn.onclick = function(){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300)|| xhr.status == 304){
result.innerHTML = xhr.responseText;
}else{
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("post", "https://www.baidu.com", true);
xhr.send(null);
}
</script>
CORS主要需要在后端进行设置,以PHP为例。
通过设置header()方法,“*”号表示允许任何域向服务端提交请求:
header( " Access-Control-Allow-Origin: * " );
也可以设置指定的域名,如域名https://www.baidu.com,那么就允许来自这个域名的请求。
header( " Access-Control-Allow-Origin: https://www.baidu.com" );
通过跨域XHR对象可以访问status和statusText属性,而且还支持同步请求。跨域XHR对象也有一些限制,但为了安全这些限制是必需的。
1、不能使用setRequestHeader()设置自定义头部
2、不能发送和接收cookie
3、调用getAllResponseHeaders()方法总会返回空字符串
由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对URL,在访问远程资源时再使用绝对URL。这样做能消除歧义,避免出现限制访问头部或本地cookie信息等问题。
Preflight
CORS通过一种叫做Preflighted Requests(预检请求)的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的方法,以及不同类型的主体内容。
在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求。这种请求使用OPTIONS方法,发送下列头部
1、Origin:与简单的请求相同
2、Access-Control-Request-Method:请求自身使用的方法
3、Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔。
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通。
1、Access-Control-Allow-Origin:与简单的请求相同
2、Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔
3、Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔
4、Access-Control-Max-Age:应该将这个Preflight请求缓存多长时间(以秒表示)
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
带凭据请求
默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。
如果服务器接受带凭据的请求,会用下面的HTTP头部来响应:
Access-Control-Allow-Credentials: true
开发者必须在AJAX请求中打开withCredentials属性
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JS(于是,responseText中将是空字符串,status的值为0,而且会调用onerror()事件处理程序)。
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
跨浏览器
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
//标准浏览器
if("withCredentials" in xhr){
xhr.open(method, url, true);
//IE10-浏览器
}else if(typeof XDomainRequest != "undefined"){
xhr = new XDomainRequest();
xhr.open(method, url);
}
return xhr;
}
ajax跨域的表现
第一种现象:No 'Access-Control-Allow-Origin' header is present on the requested resource
,并且The response had HTTP status code 404
。
出现这种情况的原因如下:
本次ajax请求是“非简单请求”,所以请求前会发送一次预检请求(OPTIONS)
服务器端后台接口没有允许OPTIONS请求,导致无法找到对应接口地址。
解决方案: 后端允许options请求
第二种现象:No 'Access-Control-Allow-Origin' header is present on the requested resource
,并且The response had HTTP status code 405
。
这种现象和第一种有区别,这种情况下,后台方法允许OPTIONS请求,但是一些配置文件中(如安全配置),阻止了OPTIONS请求,才会导致这个现象。
解决方案: 后端关闭对应的安全配置
第三种现象:No 'Access-Control-Allow-Origin' header is present on the requested resource
,并且status 200。
这种现象和第一种和第二种有区别,这种情况下,服务器端后台允许OPTIONS请求,并且接口也允许OPTIONS请求,但是头部匹配时出现不匹配现象。
比如origin头部检查不匹配,比如少了一些头部的支持(如常见的X-Requested-With头部),然后服务端就会将response返回给前端,前端检测到这个后就触发XHR.onerror,导致前端控制台报错。
解决方案: 后端增加对应的头部支持。
第四种现象:heade contains multiple values '*,*'
表现现象是,后台响应的http头部信息有两个Access-Control-Allow-Origin:*
这种问题出现的主要原因就是进行跨域配置的人不了解原理,导致了重复配置。
图片Ping
在CORS出现以前,要实现跨域Ajax通信颇费一些周折。开发人员想出了一些办法,利用DOM中能够执行跨域请求的功能,在不依赖XHR对象的情况下也能发送某种请求。
图像Ping跨域请求技术是使用<img>标签。一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。也可以动态地创建图像,使用它们的onload和onerror事件处理程序来确定是否接收到了响应。
动态创建图像经常用于图像Ping:图像Ping是与服务器进行简单、单向的跨域通信的一种方式。 请求的数据是通过査询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的。
图像Ping最常用于跟踪用户点击页面或动态广告曝光次数。图像Ping有两个主要的缺点,一是只能发送GET请求,二是无法访问服务器的响应文本。因此,图像Ping只能用于浏览器与服务器间的单向通信。
<input id="btn" type="button" value="跨域请求">
<div id="result"></div>
<script>
var add = (function(){
var counter = 0;
return function(){
return ++counter;
}
})();
btn.onclick = function(){
var sum = add();
var img = result.getElementsByTagName('img')[0];
if(!img){
var img = new Image();
}
img.height="100";
img.onload = img.onerror = function(){
result.appendChild(img);
var oSpan = document.getElementById('sum');
if(!oSpan){
oSpan = document.createElement('span');
oSpan.id="sum";
}
oSpan.innerHTML = '发送请求的次数:' + sum;
result.appendChild(oSpan);
};
if(sum%2){
img.src = "http://7xpdkf.com1.z0.glb.clouddn.com/eg_bulboff.gif?sum="+sum;
}else{
img.src = "http://7xpdkf.com1.z0.glb.clouddn.com/eg_bulbon.gif?sum="+sum;
}
}
</script>
JSONP
JSONP之所以在开发人员中极为流行,主要原因是它非常简单易用,老式浏览器全部支持,服务器改造非常小。与图像Ping相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。
不过,JSONP也有两点不足:首先,JSONP是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃JSONP调用之外,没有办法追究。因此在使用不是自己运维的Web服务时,一定得保证它安全可靠;其次,要确定JSONP请求是否失败并不容易。虽然HTML5给<script>元素新增了一个onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。但就算这样也不能尽如人意,毕竟不是每个用户上网的速度和带宽都一样。
百度搜索框就是使用了JSONP的技术,在百度搜索的URL中,有用的查询如下
https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=123&&cb=a
结果为:
a({q:"123",p:false,s:["12306","12306铁路客户服务中心","12308汽车订票官网","12306火车票网上订票官网","12333","12315","12345","12333社保查询网","123网址之家","12366"]});
iframe
jsonp是使用script标签,imgPing是使用image标签,接下里使用iframe标签实现跨域。
iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的DOM。
如果两个窗口一级域名相同,只是二级域名不同,可以通过设置document.domain
来使其通信。
锚点值,又称为片段标识符(fragment identifier),指的是URL的#号后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。父窗口可以把信息,写入子窗口的锚点值。
HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 APIpostMessage()
。
websocket
websocket是HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议,它基于TCP传输协议,并复用HTTP的握手通道,是用以在网页浏览器和服务器建立一个 socket 连接的API。通俗地讲:在客户端和服务器保有一个持久的连接,两边可以在任意时间开始发送数据。
websocket的优势
支持双向通信,实时性更强。
更好的二进制支持。
较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。
socket.io
socket.io是一个websocket库,包括了客户端的js和服务器端的nodejs,它的目标是构建可以在不同浏览器和移动设备上使用的实时应用。
socket.io的特点
易用性:socket.io封装了服务端和客户端,使用起来非常简单方便。
跨平台:socket.io支持跨平台,这就意味着你有了更多的选择,可以在自己喜欢的平台下开发实时应用。
自适应:它会自动根据浏览器从WebSocket、AJAX长轮询、Iframe流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达IE5.5。