JS之AJAX篇-CORS

引入

默认情况下,出于安全考虑,XHR对象只能访问与包含它的页面同一个域中的资源。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的

CORS(Cross-Origin Resource Sharing)跨源资源共享是W3C的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

简单请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)

同时满足下面两个条件的属于简单请求:

  1. 请求方式是GET、POST、HEAD中的一种
  2. 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对象也有一些限制:

  1. 不能发送和接收cookie
  2. 调用getAllResponseHeaders()方法总会返回空字符串
  3. 不能使用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;
}
posted @ 2021-09-29 14:18  wmui  阅读(378)  评论(0编辑  收藏  举报