目录
跨源资源共享
通过XHR进行Ajax通信的一个主要限制是跨源安全策略。默认情况下,XHR只能访问与发起请求的页面在同一个域内的资源。这个安全限制可以防止某些恶意行为。不过,浏览器也需要支持合法跨源访问的能力。
跨源资源共享(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。CORS背后的基本思路就是使用自定义的HTTP头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。
对于简单的请求,比如GET或POST请求,没有自定义头部,而且请求体是text/plain类型,这样的请求在发送时会有一个额外的头部叫Origin。Origin头部包含发送请求的页面的源(协议、域名和端口),以便服务器确定是否为其提供响应。
Origin头部的一个示例:
Origin: http://www.nczonline.net
如果服务器决定响应请求,那么应该发送Access-Control-Allow-Origin头部,包含相同的源;或者如果资源是公开的,那么就包含"*"。
例如:
Access-Control-Allow-Origin: http://www.nczonline.net
如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器就会处理这个请求。注意,无论请求还是响应都不会包含cookie信息。
现代浏览器通过XMLHttpRequest对象原生支持CORS。在尝试访问不同源的资源时,这个行为会被自动触发。要向不同域的源发送请求,可以使用标准XHR对象并给open()方法传入一个绝对URL
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert(" 请求失败,响应回来的状态码是: " + xhr.status)
}
}
}
xhr.open("get", "http://www.somewhere-else.com/page/", true)
xhr.send(null)
跨域XHR对象允许访问status和statusText属性,也允许同步请求。出于安全考虑,跨域XHR对象也施加了一些额外限制。
- 不能使用setRequestHeader()设置自定义头部。
- 不能发送和接收cookie。
- getAllResponseHeaders()方法始终返回空字符串。
因为无论同域还是跨域请求都使用同一个接口,所以最好在访问本地资源时使用相对URL,在访问远程资源时使用绝对URL。这样可以更明确地区分使用场景,同时避免出现访问本地资源时出现头部或cookie信息访问受限的问题。
预检请求
CORS通过一种叫预检请求(preflighted request)的服务器验证机制,允许使用自定义头部、除GET和POST之外的方法,以及不同请求体内容类型。在要发送涉及上述某种高级选项的请求时,会先向服务器发送一个“预检”请求。这个请求使用OPTIONS方法发送并包含以下头部。
Origin:与简单请求相同。
Access-Control-Request-Method:请求希望使用的方法。
Access-Control-Request-Headers:(可选)要使用的逗号分隔的自定义头部列表。
// 下面是一个假设的POST请求,包含自定义的NCZ头部:
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
在这个请求发送后,服务器可以确定是否允许这种类型的请求。服务器会通过在响应中发送如下头部与浏览器沟通这些信息。
Access-Control-Allow-Origin: | 与简单请求相同。 |
Access-Control-Allow-Methods: | 允许的方法(逗号分隔的列表)。 |
Access-Control-Allow-Headers: | 服务器允许的头部(逗号分隔的列表)。 |
Access-Control-Max-Age: | 缓存预检请求的秒数。 |
// 例如
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的请求时才会多发送一次额外的HTTP请求。
凭据请求
默认情况下,跨源请求不提供凭据(cookie、HTTP认证和客户端SSL证书)。可以通过将withCredentials属性设置为true来表明请求会发送凭据。如果服务器允许带凭据的请求,那么可以在响应中包含如下HTTP头部:
Access-Control-Allow-Credentials: true
如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给JavaScript(responseText是空字符串,status是0,onerror()被调用)。注意,服务器也可以在预检请求的响应中发送这个HTTP头部,以表明这个源允许发送凭据请求。
替代性跨源技术
CORS出现之前,实现跨源Ajax通信是有点麻烦的。开发者需要依赖能够执行跨源请求的DOM特性,在不使用XHR对象情况下发送某种类型的请求。虽然CORS目前已经得到广泛支持,但这些技术仍然没有过时,因为它们不需要修改服务器。
图片探测
图片探测是利用<img>标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的onload和onerror事件处理程序得知何时收到响应。
这种动态创建图片的技术经常用于图片探测(image pings)。图片探测是与服务器之间简单、跨域、单向的通信。数据通过查询字符串发送,响应可以随意设置,不过一般是位图图片或值为204的状态码。浏览器通过图片探测拿不到任何数据,但可以通过监听onload和onerror事件知道什么时候能接收到响应。下面看一个例子:
let img = new Image()
img.onload = img.onerror = function() { alert("Done!") }
img.src = "http://www.example.com/test?name=Nicholas"
这个例子创建了一个新的Image实例,然后为它的onload和onerror事件处理程序添加了同一个函数。这样可以确保请求完成时无论什么响应都会收到通知。设置完src属性之后请求就开始了,这个例子向服务器发送了一个name值。
图片探测频繁用于跟踪用户在页面上的点击操作或动态显示广告。当然,图片探测的缺点是只能发送GET请求和无法获取服务器响应的内容。这也是只能利用图片探测实现浏览器与服务器单向通信的原因。
JSONP
JSONP是“JSON with padding”的简写,是在Web服务上流行的一种JSON变体。JSONP看起来跟JSON一样,只是会被包在一个函数调用里,比如:
callback({ "name":"Nicholas" });
JSONP格式包含两个部分:回调和数据。回调是在页面接收到响应之后应该调用的函数,通常回调函数的名称是通过请求来动态指定的。而数据就是作为参数传给回调函数的JSON数据。下面是一个典型的JSONP请求:
http://freegeoip.net/json/?callback=handleRespons
这个JSONP请求的URL是一个地理位置服务。JSONP服务通常支持以查询字符串形式指定回调函数的名称。比如这个例子就把回调函数的名字指定为handleResponse()。
JSONP调用是通过动态创建<script>元素并为src属性指定跨域URL实现的。此时的<script>与<img>元素类似,能够不受限制地从其他域加载资源。因为JSONP是有效的JavaScript,所以JSONP响应在被加载完成之后会立即执行。比如下面这个例子:
function handleResponse(response) {
console.log(`You're at IP address ${response.ip}, which is in ${response.city},${response.region_name}`)
}
let script = document.createElement("script")
script.src = "http://freegeoip.net/json/?callback=handleResponse"
document.body.insertBefore(script, document.body.firstChild)
这个例子会显示从地理位置服务获取的IP地址及位置信息。
JSONP由于其简单易用,在开发者中非常流行。相比于图片探测,使用JSONP可以直接访问响应,实现浏览器与服务器的双向通信。不过JSONP也有一些缺点。
首先,JSONP是从不同的域拉取可执行代码。如果这个域并不可信,则可能在响应中加入恶意内容。此时除了完全删除JSONP没有其他办法。在使用不受控的Web服务时,一定要保证是可以信任的。
第二个缺点是不好确定JSONP请求是否失败。虽然HTML5规定了<script>元素的onerror事件处理程序,但还没有被任何浏览器实现。为此,开发者经常使用计时器来决定是否放弃等待响应。这种方式并不准确,毕竟不同用户的网络连接速度和带宽是不一样的。