一、跨域的产生背景
1、什么是同源策略?
同源,指的是两个页面的协议、端口和域名都相同,则两个页面具有相同的源。
同源策略限制了从同一个源加载的文档或者脚本如何与来自另一个源的资源进行交互。
同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。只有同一个源的脚本赋予dom、读写cookie、session和ajax等操作的权限。
2、为什么src属性加载的资源可以实现跨域?
不受同源策略的限制,带有`src`属性的标签加载时,实际上是由浏览器发起的一次GET请求,不同于XMLHTTPRequest,他们是通过src属性加载的资源。浏览器限制了JavaScript的权限,使其【不能读、写】其中返回的内容。
对于XMLHTTPRequest,受同源策略的限制,它可以访问同源对象的内容,但是不能访问跨域资源。
W3C制定了XMLHTTPRequest跨域访问标准,它需要目标域返回的http头来授权是否允许跨域访问,因为http头对JS来说是无法控制的。
3、跨源网络访问
同源策略限制了不同源之间的交互,这些交互通常分为三类:
- 通常允许跨域写操作
- 通常允许跨域资源嵌入
- 通常不允许跨域读操作
可以嵌入跨源的资源
- 【<script src="..."></script>】标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。
- 【<link rel="stylesheet" href="..."/>】标签嵌入CSS,CSS的跨域需要一个设置正确的Content-type消息头。
- 【<img src="...">】嵌入图片
- 【<video>和<audio>】嵌入多媒体资源。
- 【@font-face】引入的字体
- 【<frame>和<iframe>】载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。
二、常见的跨域方法
【JSONP跨域】
1、JSONP实现跨域的原理
在JS中,对于<script><img><link></iframe>
等标签都可以跨域加载资源,这是因为对于这些标签的src或者href属性加载资源时,不受同源策略的限制,浏览器不会限制内联脚本的执行。
使用JSONP实际上是由浏览器发起了一次get请求,通过在src属性的url后边添加一个回调函数。对于XMLHttpRequest发起的请求,浏览器限制了JS的权限,使其不能读写其返回的内容;XMLHttpRequest可以访问同源对象的内容,但是由于同源策略的约束,不能访问跨域资源。
W3C制定了xhr跨域访问的标准,它需要目标域返回的http头来授权是否允许跨域访问,http头对于JS来说是无法控制的。
2、JSONP组成
JSONP由两部分组成:回调函数和数据。
回调函数是当响应到来时应该在页面中调用的函数; 数据就是传入回调函数中的JSON数据
3、JSONP的优缺点
优点:
- 兼容性好,可用于解决主浏览器的跨域数据访问的问题;
- 能够直接访问响应文本,支持在浏览器与服务器之间双向通信;
缺点:
- 存在安全隐患,JSONP是从其他域中加载代码执行,如果其他域不安全,则不安全;可能会遭受XSS攻击;
- 确定JSONP请求是否失败不容易
4、JSONP的实现流程
- 声明一个回调函数,其函数名当作参数值,要传递给跨域请求数据的服务器,函数行参为要获取的目标数据;
- 创建一个
5、函数封装
function jsonp ({url, params, callback}) {
return new Promise (function (resolve, reject) {
let script = document.createElement('script');
window[callback] = function (data) {
resolve(data);
document.body.removeChild(script);
}
params = {...params, callback};
let arr = [];
for (let key in arr) {
arr.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arr.join('&')}`;
document.body.appendChild(script);
})
}
jsonp({
url: '',
params: {},
callback: '函数名'
}).then(function (data) {
console.log(data);
})
6、实现
function getJSONP(url,callback){
//根据指定的URL发送一个JSONP请求
//然后将解析得到的响应数据发送给回调函数
var hdNum = "callback" + getJSONP.counter++;//每次自增计数器
var hdName = "getJSONP." + hdNum;//创建回调函数的名字,将计数器作为回调函数的属性
if(url.indexOf("?") === -1){
url += ">jsonp=" + hdName;
}else{
url += "&jsonp=" + hdName;
}
//创建script元素用于发送请求
var script = document.createElement("script");
getJSONP[hdName] = function(response){
try{
callback(response);
}finally{
delete getJSONP[hdNum];
script.parentNode.removeChild(script);
}
};
script.src = url;
document.body.appendChild(script);
}
getJSONP.counter = 0;
var s = document.getElementById('s');
var list = document.getElementById('list');
s.oninput = function(){
var script = document.createElement('script');
script.src = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=' + this.value+ '&json=1&p=3&sid=1432_18280_17942_21080_18559_21454_21394_21378_21191&req=2&csor=1&cb=fn' ;
document.body.appendChild(script);
};
function fn( data ){
// console.log(data);
var str = '' ;
for (var i = 0; i < data.s.length; i++ ){
str += '<li>'+ data.s[i] +'</li>' ;
}
list.innerHTML = str;
}
}
【CORS跨域】
1、ajax通信的限制
通过XHR实现Ajax通信的一个主要限制,来源于跨域安全策略。
默认情况下,XHR对象只能访问与包含它的页面位于同一个域中的资源。
2、CORS跨域资源共享
CORS是一个W3C标准,全称是“跨域资源共享”。定义了在必须访问跨源资源时,浏览器和服务器应该如何沟通。它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
3、CORS实现跨域的基本思想
使用自定义的HTTP头部,让浏览器与服务器进行沟通,从而决定请求和响应应该成功还是失败。
【注意事项】
并不一定是浏览器发起了跨站请求,也可能是请求能够正常被发送,但是返回结果被浏览器给拦截了。
跨域资源共享机制允许web应用服务器进行跨域访问控制,才保证数据传输得以安全运行。
例如:
- 在发送简单请求时,它没有自定义的头部,而主体内容是text/plain。在发送该请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。
- 如果服务器认为这个请求可以接收,就会在Access-Control-Allow-Origin头部中返回相同的源信息;如果没有这个头部,或者有头部但源信息不匹配,浏览器就会驳回请求。
【注意:请求和响应都不包含cookie信息】
4、CORS通信
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE不能低于10。
整个CORS通信过程都是浏览器自动完成,不需要用户参与。
对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。
浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。【这个请求是预检请求】
实现CORS通信的关键是服务器,只要服务器实现了CORS接口,就可以实现跨源通信。
5、CORS请求的分类
浏览器将CORS请求分为两类:简单请求和非简单请求。
5-1、简单请求
简单请求支持的方法有:HEAD、GET、POST
头信息字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type【只可以取如下值:application/x-www-form-urlencoded、multipart/form-data、text/plain】
# 简单请求的描述
对于简单请求,浏览器直接发出CORS请求,简单来说,就是在头信息之中添加一个Origin字段。
GET /cors HTTP/1.1
Origin: http://api.com
Host:api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: ...
上述的头信息中,Origin字段用来说明本次请求来自哪个源(协议+域名+端口号),服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,在不许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。这种错误无法通过状态码识别,因为HTTP回应的状态码可能是200。
# 响应字段
如果Origin指定的源,在许可范围内,服务器返回的响应,会多出几个头信息字段:
Access-Control-Allow-Origin:
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers:
Content-Type: text/html; charset=utf-8
# 服务器请求的字段
Access-Control-Allow-Origin
该字段是必须的,他的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
Access-Control-Allow-Credentials
- 该字段可选,他的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包含在CORS请求之中。设为true,即表示服务器明确许可。cookie可以包含在请求中,一起发给服务器。
- 这个值也只能设置为true,如果服务器不要浏览器发送cookie,删除该字段即可。
- 将XMLHttpRequest的withCredentials标志设置为true,向服务器发送cookies。如果服务器端的响应中未携带Access-Control-Allow-Credentials:true,浏览器将不会把响应内容返回给请求的发送者
Access-Control-Expose-Headers
- 该字段可选,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、pragma。
- 如果想要拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
withCredentials属性
- CORS请求默认不发送cookie和HTTP认证信息。如果要把cookie发送到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段,另一方面,开发者必须在AJAX请求中打开withCredentials属性【var xhr = new XMLHttpRequest();xhr.withCredentials=true;】;
- 如果不设置,即使服务器同意发送cookie,浏览器也不会发送;或者,服务器要求设置cookie,浏览器也不会处理。
- 需要注意的是:
如果要发送cookie,Access-Control-Allow-Origin就不能设置为*,必须指定明确的、与请求网页一致的域名。
同时,cookie依然遵循同源策略,只有用服务器域名设置的cookie才能上传,其他域名的cookie并不会上传,且跨源-原网页中的代码的document.cookie也无法读取服务器域名下的cookie
5-2、非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者,Content-Type的值为application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为预检请求。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定的答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
5-3、预检请求
需要预检的请求必须首先使用options方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。
预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
发送预检请求的条件:
1、发送方法如下:put delete connect options trace patch
2、设置了以下字段:
Accept Accept-Language
Content-Language
Content-Type[不是下列值:text/plain mutipart/form-data application/x-www-form-urlencoded]
3、请求中的XMLHttpRequestUpload对象注册了任意多个事件监听器
4、请求中使用了ReadableStream对象
var url = '';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', true);
xhr.send();
浏览器发现上述请求是一个非简单请求,就会自动发出一个预检请求,要求服务器确认可以这样请求,
预检请求用的方法是OPTIONS,表示这个请求是用来询问的,头信息里面的关键字段是Origin,表示请求来自哪个源;除了Origin,预检请求的头信息必须包含以下两个字段:
Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法
Access-Control-Request-Headers: 该字段值是一个用逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段
# 预检请求的回应
服务器收到预检请求以后,检查了Origin、Access-Control-Request-Method、Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
如果浏览器否定了预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XHR的onerror函数捕获
# 服务器回应的相关字段
Access-Control-Allow-Methods
该字段必须,他的值是一个以逗号分隔的字符串,表明服务器支持的所有跨域请求的方法。注意:返回的是所有支持的方法,而不单是浏览器请求的那个方法,这是为了避免多次预检请求
Access-Control-Allow-Headers
该字段是必须的,它的值是一个以逗号分隔的字符串,表示服务器支持的所有头信息字段,不限于浏览器在预检请求中的字段
Access-Control-Allow-Credentials
是否允许存储cookie和发送HTTP凭证
Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒,在有效期内,不需要发出另一条预检请求
6、跨域请求功能概述
跨域资源共享标准新增了一组HTTP首部字段,允许服务器声明哪些源站通过浏览器有有限访问哪些资源。
根据规范,对于那些可能对服务器数据产生副作用的HTTP请求方法【特别是GET以外的请求,或者搭配某些MIME类型的post请求】,浏览器必须首先使用OPTIONS方法发起一个预检请求,从而获取服务端是否允许该跨域请求。服务器确认允许后,才会发起实际的HTTP请求。
在预检请求的返回中,服务器端也可以通知客户端是否需要携带身份凭证【包含cookies和HTTP认证相关数据】
CORS请求失败会产生错误,但是为了安全,在JS代码层面是无法获知到底是哪里出了问题。只能通过查看浏览器的控制台得知哪里出现了错误。
7、附带身份凭证的请求与通配符:
对于附带身份凭证的请求,服务器不得设置Access-Control-Allow-Origin的值为*。因为请求的头部中携带了cookie信息,如果Access-Control-Allow-Origin的值设置为*,请求将会失败。如果将他的值设置为一个具体的值,则请求将成功执行。
8、与JSONP跨域的区别:
CORS跨域比JSONP更强大,JSONP跨域只支持get请求,CORS跨域支持所有类型的HTTP请求。JSONP的优势杂居支持老式浏览器,以及向不支持CORS的网站请求数据。
【websocket跨域】
1、定义
WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通信的协议。
2、作用
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务器主动向客户端发送数据。
在WebSocket API中,浏览器和服务器之间只需要完成一次握手,两者之间就可以创建持久性连接,并进行双向数据传输。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,浏览器和服务器之间就形成了一道快速通道。两者之间就可以直接互相传送数据。
WebSocket协议能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
AJAX轮询:
在特定的时间间隔,由浏览器对服务器发出http请求,然后由服务器返回最新的数据给客户端的浏览器。
缺点:
浏览器需要不断的向服务器发出请求,然而,http请求可能包含较长的头部,真正有效的数据可能只有一小部分,会浪费很多带宽。
3、WebSocket属性
3-1、readyState
readyState,只读属性,表示连接状态,取值如下:
- 0:表示连接尚未建立;
- 1:表示连接已经建立,可以进行通信;
- 2:表示连接正在进行关闭;
- 3:表示连接已经关闭或者连接不能打开;
3-2、bufferedAmount
只读属性bufferedAmount已被send方法放入,正在等待传输,但是还没有发出UTF-8文本字节数。
4、WebSocket事件
- open:连接建立时触发;
- message:客户端接收服务端数据时触发;
- error:通信发生错误时触发;
- close:连接关闭时触发;
5、WebSocket方法
- send:使用连接发送数据;
- close:关闭连接;
6、WebSocket实例
WebSocket协议本质上是一个基于TCP的协议。
为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中,附加头信息“Upgrade:WebSocket”表明这是一个申请协议升级的HTTP请求,服务器端解析这些附加的头信息,然后产生应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在,直到客户端或服务器端的某一方主动的关闭连接。
【代码实现】
function WebSocketTest () {
if ("WebSocket" in window) {
var ws = new WebSocket("ws://localhost:1234/echo");
ws.onopen = function () {
ws.send('发送数据');
}
ws.onmessage = function (data) {
console.log(data.data);
}
ws.onclose = function () {
}
}
}
【其他】
WebSocket使用和http相同的TCP端口,可以绕过大多数防火墙的限制。
默认情况下,WebSocket协议使用80端口,运行在TSL之上时,默认使用443端口。
Socket是传输控制层协议,WebSocket是应用层协议。
【postMessage跨域】
1、定义
window.postMessage()是HTML5 XMLHttpRequest level2中的API,且是为数不多的可以跨域的window属性之一;
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文档、多窗口、跨域消息传递;
window.postMessage()方法可以安全的实现跨源通信。通常,对于两个不同页面的脚本,只有当执行他们的页面位于具有相同的协议、端口、域名时,这两个脚本才能相互通信。
window.postMessage()方法被调用时,会在页面所有脚本执行结束之后,向目标窗口派发一个MessageEvent消息。该MessageEvent消息有4个属性需要注意:
- message属性表示该message的类型;
- data属性为window.postMessage()的第一个参数;
- origin属性表示window.postMessage()方法执行时,调用页面的当前状态;
- source属性表示调用window.postMessage()方法的窗口信息;
2、解决的问题
- 页面和其打开的新窗口的数据传递;
- 多窗口之前消息传递;
- 页面与嵌套的iframe消息传递;
- 跨域数据传递;
3、语法
otherWindow.postMessage(message, targetOrigin, [transfer])
【参数描述】:
- message:将要发送到其他window的数据;可以不受限制的将数据对象安全的传送到目标窗口而无需序列化;
- targetOrigin:指定哪些窗口可以来接收到消息事件;其值可以是字符串或者URI;
如果明确的知道消息应该发送到哪个窗口,那么targetOrigin应该有一个确切的值,而不是*,不提供确切的目标将导致数据泄漏到任何对数据感兴趣的恶意站点。
4、实现
// a.html
<iframe src="http://localhost:9000/b.html" id="iframe" "load()"></iframe>
<script type="text/javascript">
function load () {
var iframe = document.getElementById('iframe');
console.log(iframe);
window.postMessage('123', 'http://localhost:9000');
window.onmessage = function (e) {
console.log(e.data);
}
}
</script>
// b.html
<script type="text/javascript">
window.onmessage = function (e) {
console.log(e.data);
e.source.postMessage('456', e.origin);
}
</script>