文章目录
同源策略
在了解跨域这个概念之前首先要知道的是何为同源策略。所谓的同源是一种安全机制,为了预防某些恶意行为(例如 Cookie 窃取等),浏览器限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。而满足同源要具备三方面:协议相同、域名相同、端口相同。
以下是对于http://domain.com/dir/index.html(默认端口 80)来进行同源判断:
http://domain.com/dir2/info.html(同源)
https://domain.com/dir/index.html(非同源,协议不相同)
http://www.domain.com/dir/index.html(非同源,域名不同)
http://domain.com:233/dir/index.html(非同源,端口不同)
什么地方有要求同源
- Ajax 通信
- Cookie
- LocalStorage
- IndexDB
- DOM 的操作
跨源资源共享(CORS)
同源策略对于用户信息安全是必不可少的,但是实现合理的跨域请求也是很重要的,于是 W3C 就定了一个叫CORS(Cross-Origin Resource Sharing)的草案,也就是跨域资源共享。其基本思想就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功或是失败。
CORS 的简单请求原理
例如发送一个简单的GET或POST请求,浏览器会为其添加一个Origin的头,其包含页面的源信息(协议、域名和端口),以便这个服务器根据这个头部信息来决定是否给予响应,下面是一个Origin 头部的示例:
Origin: http://domain.com
若服务器认为该请求可接受,就在Access-Control-Allow-Origin
头部中回发相同的源信息(如果是公共资源,则将该头部设为*,但是它们都不发送 Cookie)。要注意的是请求和响应都不包含 Cookie 信息。
Access-Control-Allow-Origin: http://domain.com
如果没有这个头部,或者有这个头部但是源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器都会处理请求。
注意 请求和响应都不包含cookie 信息
浏览器对CORS 的实现
浏览器通过XMLHttpRequest 对象实现了对CORS 的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源时,使用标准的XHR对象并在open() 方法中传入绝对URL 即可
通过跨域XHR 对象可以访问status 和statusText 属性,而且还支持同步请求。
跨域XHR 对象也有一些限制(为了安全的必须限制):
- 不可使用setRequestHeader()设置自定义头部。
- 默认情况下不能请求 Cookie 等凭据,除非服务器在响应头中将Access-Control-Allow-Credentials设为true。
- 调用getAllResponseHeaders()会返回空字符串。
由于对于同源请求还是跨源请求都使用的是相同的接口,因此
- 对于本地资源,使用相对URL
- 访问远程资源,使用绝对URL
这样可以消除歧义,避免出现限制访问头部或者本地cookie 信息等问题
通过透明服务器机制使用自定义头部(Preflighting Requests)
Preflighting Requests 支持开发人员使用自定义头部、get、post 之外的方法,以及不同类型的主体内容。在使用高级选项发送请求时,就会向服务器发送一个Preflight 请求。这种请求使用OPTIONS 方法,发送下列头部:
Origin:与简单的请求相同
Access-Control-Request-Method: 请求自身使用的方法
Access-Control-Requset-Headers: (可选)自定义的头部信息,多个以逗号分隔
例:下面是一个使用自定义头部ncz 的使用post 方法发送的请求
Origin:http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Requset-Headers: NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通
Access-Control-Allow-Origin: 与简单的请求相同
Access-Control-Allow-Method: 允许的方法,多个方法以逗号分隔
Access-Control-Allow-Headers: 允许的头部
Access-Control-Max-Age:应该将这个Preflight请求缓存多长时间(以秒表示)
例如:
Access-Control-AllowOrigin: http://www.nczonline.net
Access-Control-Allow-Method: POST , GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age:1728000
Preflight 请求结束后,结果将按照响应中的指定的时间缓存起来。而为此付出代价只是第一次发送,这种请求时会多一次HTTP请求。
对请求设置凭据
默认情况下。跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等),通过将withCredentials 属性设置为true ,可以指定某些请求应该发送凭据的请求,会用下面的HTTP 头部来响应
Access-Control-Allow-Credentials: true
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给js,另外服务器还可以在Preflight 响应中发送这个HTPP 头部,表示允许发送带凭据的请求
跨浏览器的CORS
因为每个浏览器对CORS 的支持程度不一样, 但所有浏览器都支持简单的请求(非Preflight 和不带凭据的请求),所以需要实现一个方案:检测XHR 是否支持CORS 的最简单方式(即检查withCredentials属性),再结合检测XDomainRequest 对象是否存在。这样就可以兼顾所有浏览器了。
其他跨域技术
1. 图像Ping
图像Ping 是与服务器进行简单、单向的跨域通信的一种方式。
一个网页可以从任何网页中加载图像,不用担心跨域不跨域。也可以动态创建图像,使用他们的onload 和 onerror 事件处理程序 来确定是否收到了响应
该跨域技术主要是利用<img>
标签设置src
属性(请求地址通常都带有查询字符串),然后监听该<img>
的onload
或onerror
事件来判断请求是否成功。响应的内容通常是一张 1 像素的图片或者204响应。
图片 Ping 有两个缺点:
- 因为是通过
<img>
标签实现,所以只支持GET请求。 - 无法访问服务器响应脚本,只能用于在浏览器与服务器之间进行单向通行。
由于以上特点,图片 Ping 方法常用于跟踪用户点击页面或动态广告的曝光次数。
2. JSONP
JSONP 由两部分组成:
- 回调函数:当响应到来时应该在页面中调用的函数
回调函数的名字一般是在请求中指定的 - 数据:指传入回调函数中的JSON 数据
主要原理
通过动态创建<script>
标签,通过src 属性指定一个跨域URL 。(这里的<script>
和<img>
元素类似,都有能力不受限制从其他域里加载资源),因为JSONP 是有效的js 代码,所以服务器收到请求后,将数据放在一个指定名字的回调函数中并传送回来。
浏览器:
//对创建标签行为进行封装
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://domain.com/data?callback=getdata')
}
//服务器收到上面的请求后,将数据放在回调函数的参数(data)中返回
function getdata(data) {
console.log(data)
}
服务器:
//服务器获取参数名后,将回调函数和参数拼接为字符串返回
response.send(
`${query.callback}({
"name": "Hello"
})`
)
JSONP与图像Ping 相比的优点是:
在于能够直接响应文本,支持在浏览器与服务器之间双向通信
不足:
- 因为JSONP 是从其他域中加载代码执行的,如果其他域不安全,则请求回来的响应中会夹杂一些恶意代码(解决办法:只能完全放弃JSONP调用)
- 要确定JSONP 请求是否失败并不容易(使用计时器监测指定时间内是否收到了响应【不是每个用户上网的速度和带宽都一样】)
3. Comet
Ajax 是一种从页面向服务器请求数据的技术
Comet 是一种服务器向页面推送数据的技术(能够让信息实时的被推送到页面上,适合处理体育比赛的分数和股票报价)
实现方式
a. 长轮询(为短轮询的一个翻版)
首先介绍一下短轮询(下图为短轮询和长轮询的区别)
可以看到长轮询把短轮询颠倒了一下:页面发送一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发送一个到服务器的新请求,这个过程一直持续不断。
注意:
不管是长轮询还是短轮询,浏览器都要在接收数据之前,先对服务器发起连接
区别:
短轮询是服务器立即发送响应,无论数据是否有效
长轮询是等待发送响应
轮询的优点:
- 所有浏览器都支持
- 使用XHR 和setTimeout() 就能实现(而开发者需要做的就是决定什么时候发送请求)
b. 流(HTTP 流)
流不同于上面的两种轮询,因为它在页面的整个生命周期只有一个HTTP 连接
具体操作
浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性的向浏览器发送数据
** 实现原因**
所有服务器都支持打印到输出缓存然后刷新(即输出缓存中的内容一次性全部发送到客户端)的功能
实现方式
通过侦听readystatechange事件及检测readystate 的值是否为3 ,就可以利用XHR 对象实现HTTP 流。
随着不断从服务器接收数据,readystate 的值就会周期的变为3。当readystate 值变为3时,responseText 属性中就会保存接收到的所有数据。此时,将就需要比较之前接收到的数据,决定从上什么时候位置开始取得最新的数据
4. SSE (服务器发送事件)
SSE API用于创建到服务器的单向连接
服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME 类型必须是 text/event-stream,而且是浏览器中的js API 能解析格式输出。
SSE 支持短轮询、长轮询和HTTP 流,而且能在断开连接时自动确定何时连接
传递方式
- 预定新的事件流
首先创建一个新的 EventSource 对象,并传入一个入口点
var source = new EventSource("myevents.php")
注意
传入的URL 必须与创建对象的页面同源(相同的URL 模式,域及端口)
EventSource 的实例有一个readyState 属性,属性值有以下作用意义:
- 0 表示正在连接到服务器
- 1 表示打开了连接
- 2 表示关闭了连接
另外还有三个事件
- open:在建立连接时触发
- message:在从服务器接收到新事件时触发
- error:在无法建立连接时触发
- 指定事件
source.onmessage = function(event){
var data = event.data;
//处理数据(服务器发回的数据以字符串保存在event.data中)
}
- 关闭连接
默认情况下,EvnetSource 对象会保持与服务器的活动连接。如果连接断开,还会重新连接。
这就意味着SSE 适合长轮询和 http 流。如果想强制立即断开并且不再重新连接,可以调用close()方法
source.close()
5. Web Sockets
在一个单独持久的连接上提供全双工,双向通信
方式:
- 在js 中创建了Web Sockets 之后,会有一个HTTP 请求发送到浏览器以表示发起连接
- 取得服务器标准响应后,建立的连接会使用HTTP 升级,从HTTP 升级到Web Socket 协议
(这句话的意思是:标准的HTTP 服务器无法实现Web Sockets,只有支持这种协议的专门服务器才能正常工作)
由于 Web Sockets使用了自定义协议,所以URL 模式也略有不同。未加密不再是http://
而是ws://
;加密也不在试http://
而是 wss://
使用 Web Sockets协议的好处是:
- 能在客户端和服务器之间发送非常少量的数据,而不用担心HTTP 那样字节的开销
- 适合移动应用(传递的数据包非常小)
使用 Web Sockets协议的缺点是:
- 指定协议的时长比 js API 时间还要长
使用 Web Sockets:
- 实例一个WebSocket对象并传入一个要连接的URL
var socket = new WebSocket("ws://www.example.com/server.php")
注意:
- 必须要给WebSocket 构造函数传入绝对URL
- 同源策略对 Web Sockets 不适用,因此可以通过 WebSocket对象打开任何站点的连接
- 至于是否与某个域中的页面连接,完全取决于服务器(通过握手协议就可以知道请求来自哪)
- 实例化WebSocket对象之后,浏览器马上就会尝试连接
WebSocket有一个表示当前状态的readystate 属性
- WebSocket.OPENING(0) 正在建立连接
- WebSocket.OPEN(1) 已经建立连接
- WebSocket.CLOSING(2) 正在关闭连接
- WebSocket.CLOSE(3) 已经关闭连接
WebSocket 没有readystatechange 事件,不过他有其他事件,对应着不同的状态。readystate的值永远从0 开始
- 关闭 WebSocket
socket.close()
调用这个函数之后,readystate 的值就立即变为2 (正在关闭),而在关闭之后就会变成3
- 发送和接收数据
WebSocket 打开之后,就可以通过连接发送和接收数据
发送使用send方法传入任意字符串
var socket = new WebSocket("ws://www.example.com/server.php")
socket.send("hello orld")
因为WebSocket 只能发送纯文本数据,对于复杂的数据,在通过连接发送之前,必须进行序列化
- 服务器要读取其中的数据,就要解析收到的JSON 字符串
- 当服务器向客户端发来消息时,WebSocket 对象就会触发message 事件,这个message 事件与其他传递消息的协议类似,也是把数据保存在evenet.data中
socket.onmessage = function(event){
var data = event.data;
//处理数据
}
与send 发送数据一样,event.data 中返回的数据也是字符串。如果想要得到其他格式的数据,必须手工解析这些数据
WebSocket 的其他事件
- open:在成功建立连接时触发
- error:在发生错误时触发,连接不能持续
- close:在连接关闭时触发(这个事件的event 对象有额外的信息)
这三个事件有额外的属性- wasClean(布尔值)表示连接是否已解决明确的关闭
- code 是服务器返回的数值状态码
- reason 是一个字符串,包含服务器发回的消息
SSE 与Web Sockets
在使用这两个协议之前需要考虑:
- Web Sockets 不同于HTTP ,现有服务器不能用于Web Sockets通信
- 是不是需要双向通信
SSE:只读取服务器(比赛成绩)
Web Socket:双向通信(聊天室)
注意:组合XHR 和SSE 也是能够实现双向通信