JS之AJAX篇-CORS
引入
默认情况下,出于安全考虑,XHR对象只能访问与包含它的页面同一个域中的资源。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的
CORS(Cross-Origin Resource Sharing)跨源资源共享是W3C的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
简单请求
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)
同时满足下面两个条件的属于简单请求:
- 请求方式是GET、POST、HEAD中的一种
- Content-Type是application/x-www-form-urlencoded、multipart/form-data、text/plain中的一种,并且不能包含默认头部外的其他自定义头部
对于简单请求,浏览器不会触发CORS预检请求(后面会说预检请求)。而非简单请求会触发CORS预检请求。
默认情况下,在发送XHR请求的同时,会发送下列头部信息:
Accept: 浏览器能够处理的内容类型
Accept-Charset: 浏览器能够显示的字符集
Accept-Encoding: 浏览器能够处理的压缩编码
Accept-Language: 浏览器当前设置的语言
Connection: 浏览器与服务器之间连接的类型
Cookie: 当前页面设置的任何Cookie
Host: 发出请求的页面所在的域
User-Agent: 浏览器的用户代理字符串
Referer: 发出请求的页面的URI
但是如果是CORS请求,无论是请求还是响应,都不包含cookie(很重要)。这也就是为什么前后端分离项目使用token来携带验证信息。如果在头部中添加了自定义字段(如Token字段),请求就属于非简单请求,由于非简单请求会触发CORS预检请求,所以会请求两次接口
CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。当发出CORS请求时,头部信息中会额外附加一个origin字段,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应
浏览器如果发现跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求
如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中回发相同的源信息(如果是公共资源,可以回发"*" )
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。但是浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。
原生支持
标准浏览器都通过XMLHttpRequest对象实现了对CORS的原生支持,所以要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可
注意: IE9-浏览器不支持
btn.onclick = function() {
AJAX({
url: 'http://127.0.0.1:3040/api/test', // 绝对URL
method: 'GET',
headers: {
'Content-Type': 'text/plain'
},
callback: function(data) {
console.log(data)
}
})
}
function AJAX(obj) {
var method = obj.method || 'GET',
headers = obj.headers || {},
data = obj.data || {},
url = obj.url || '';
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
if((xhr.status >= 200 && xhr.readyState < 300) || xhr.status == 304) {
obj.callback && obj.callback(xhr.responseText)
}
}
}
if((obj.method).toUpperCase() == 'GET') {
// 编码
for(var key in data) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
}
// url += '&' + Date.now(); // 随机时间戳,防止请求缓存
}
xhr.open(method, url, true);
// 设置header
for(var header in headers) {
xhr.setRequestHeader(header, headers[header]);
}
if((obj.method).toUpperCase() == 'GET') {
xhr.send(null);
}else{
xhr.send(JSON.stringify(data));
}
}
CORS需要在后端进行设置,以Nodejs为例(express框架),安装下cors包即可
app.use(cors({
origin: 'http://127.0.0.1:3030'
}))
出于安全限制,跨域XHR对象也有一些限制:
- 不能发送和接收cookie
- 调用getAllResponseHeaders()方法总会返回空字符串
- 不能使用setRequestHeader()设置自定义头部(仅限简单请求)
开发中,对于本地资源,最好使用相对URL,在访问远程资源时再使用绝对URL。这样做能消除歧义,避免出现限制访问头部或本地cookie信息等问题
Preflight
CORS通过一种叫做Preflighted Requests(预检请求)的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的方法,以及不同类型的主体内容
注意: IE10-浏览器不支持
Preflight请求会使用OPTIONS方法发送下列头部
Origin: 与简单的请求相同
Access-Control-Request-Method:请求自身使用的方法
Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通
Access-Control-Allow-Origin:与简单的请求相同
Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔
Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔
Access-Control-Max-Age:应该将这个Preflight请求缓存多长时间(以秒表示)
Preflight请求结束后,结果将按照响应中指定的时间缓存起来。缓存期间不会发送Preflight请求
带凭证请求
默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。但是通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据
服务器需要设置接收带凭据的请求
Access-Control-Allow-Credentials: true
开发者需要在AJAX请求中打开withCredentials属性。否则,即使服务器同意发送Cookie,浏览器也不会发送
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
注意: 如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传
后端示例
app.use(cors({
origin: 'http://127.0.0.1:3030',
maxAge: 172800,
credentials: true
}))
app.get('/api/test', function(req, res) {
res.cookie('token', 'asdfg');
res.send('hello world')
})
浏览器兼容
兼容主要指的是IE浏览器兼容。微软引入了XDR(XDomainRquest)类型,这个对象与XHR类似,能实现安全可靠的跨域通信。XDR对象的安全机制部分实现了W3C的CORS规范
以下是XDR与XHR的一些不同之处
1、cookie不会随请求发送,也不会随响应返回
2、只能设置请求头部信息中的Content-Type字段
3、不能访问响应头部信息
4、只支持GET和POST请求
这两个对象共同的属性/方法如下
1、abort():用于停止正在进行的请求
2、onerror:用于替代 onreadystatechange 检测错误
3、onload:用于替代 onreadystatechange 检测成功
4、responseText:用于取得响应内容
5、send():用于发送请求
XDR对象的使用方法与XHR对象相似,也是创建一个XDomainRequest的实例,调用open()方法,再调用send()方法。但与XHR对象的open()方法不同,XDR对象的open()方法只接收两个参数:请求的类型和URL
兼容方案
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;
}