为什么XMLHttpRequest不能跨域请求资源

5,342 阅读6分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

本文打算重新学习一下关于跨域的问题。本文会加入自己的理解,对于十分了解的朋友,可以略过。

XMLHTTPRequest

XHR是他的简称,他的作用是用户与服务器进行交互。通过HXR在不刷新页面的情况下通过特定的URL向服务器获取数据。AJAX中大量应用。

简单封装一个AJAX

// 暂时只考虑get请求,不考虑IE的兼容
const data = {
    url: "/",
    type: "get",
    async: true,
    params: {
        username: "sun",
        age: 18
    },
    success: (res) => {
        console.log(res)
    },
    error: (res) => {
        console.log(res)
    }
};
function ajax(data) {
    const key = Object.keys(data.params);
    let params = [];
    for (let i = 0; i < key.length; i++) {
        params[i] = key[i] + "=" + data.params[key[i]];
    }
    // username=sun&age=18
    const string = params.join("&"); 
    const url = data.url + "?" + string;
    const xhr = new XMLHttpRequest();
    // 允许Cookie传递
    xhr.withCredenttials = true
    xhr.open(data.type, url, data.async);
    xhr.send();
    // 连接服务器成功立即执行此函数
    xhr.onreadystatechange = function () {
        if (this.readyState === 4 && this.status === 200) {
            data.success(this.response);
        } else {
            data.error(this.responseXML);
        }
    }
}
ajax(data);

上面是AJAX对XHR的简单应用,open()会开启和服务端的关联,send()会等待onreadystatechange()函数完全结束之后再执行关闭。当readyState改变的时候onreadystatechange()函数会被调用。

readyState有五个值:

  • 0:代理被创建,但未调用open()方法
  • 1:open()方法被调用
  • 2:send()方法被调用
  • 3:下载中,responseText属性包含部分数据
  • 4:下载操作已完成

在使用XMLHTTPRequest或者img标签时,会受到同源策略的约束。接下来我们看看同源策略。

同源策略 与 XMLHTTPRequest

同源策略是浏览器安全策略之一,用来限制一个源(origin)的文档或加载的脚本与另一个源进行交互。

同源 与 不同源(跨域)

同源是指当请求方与被请求方的协议、域名、端口三者一致的情况下,我们称为同源。

这里的域名就是域名,不是IP。域名不同,表示主机不同。端口不同,表示请求的服务不同。

如果不同源,我们称之为跨域,接下来我们来介绍如何进行跨源访问,也就是常说的跨域。

跨源网络访问

在使用XMLHTTPRequest或者img标签时,会受到同源策略的约束。

这些交互通常分为三类:

  • 跨域写操作通常是被允许的。例如link链接、重定向以及表单提交。特定的少数HTTP请求需要添加preflight

  • 跨域资源嵌入一般是被允许的。

  • 跨域读操作一般是不被允许的,但通常可以通过内嵌资源来巧妙的读取访问。 下面是可能嵌入跨源的资源的一些示例:

  • <script src="..."></script>标签嵌入跨域脚本。语法错误信息只能被同源脚本中捕捉到。

  • <link rel="stylesheet" href="...">嵌入CSS。

  • 通过img标签展示图片。

  • 通过video和audio播放媒体资源。

  • 通过<object>嵌入的插件。

  • 通过@font-face引入字体。

  • 通过<iframe>载入任何资源。

除了以上这些,还有什么方法能够允许跨域访问?

CORS

CORS(跨源资源共享)是一种基于HTTP头的机制。该机制是允许服务器标识除了自己以外的其他origin(域,协议和端口)可以访问加载这些资源,这样浏览器就可以访问加载这些资源了。

CORS是需要浏览器和服务器同时支持的。服务器需要配置请求头信息,而浏览器发现AJAX请求(XMLHTTPRequest)跨源,就会自动添加一些附加头的信息。

因此,实现CORS通信的关键是服务器,只要服务器实现了CORS接口,就可以跨源。

浏览器将CORS请求分成两类:简单请求和复杂请求。

简单请求

简单请求是不会触发CORS预检请求。以下是简单请求:

  • 使用如下方法之一:

    • GET
    • HEAD
    • POST
  • 用户设置的字段不会超过如下:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-type: 只限于三种,text/plan、multipart/form-data、application/x-www-form-urlencoded

以上是为了兼容表单form,因为历史上表单是一直可以跨域请求的。所以AJAX设计是只要表单可以发,AJAX就可以发。

简单请求基本流程 浏览器会在简单请求的头部中加入一个字段Origin,来说明本次请求来自于哪个源。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器根据Origin字段判断是否能够进行这次请求,如果 Origin 指定的源不在服务器的允许范围之内,服务器会返回给浏览器一个正常的HTTP回应。而不是包含 Access-Control-Allow-Origin 字段的回应。

浏览器发现响应头的信息中没有包含Access-Control-Allow-Origin字段,就知道是错误的,就会被HMLHttpRequest的onerror回调函数捕获。

如果Origin指定的源在可允许的范围内,服务器会在响应头中加入几个信息:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

这些信息的含义:

  • Access-Control-Allow-Origin

    • 这个字段是必须存在的,表示允许哪个origin跨源请求,可以是*,表示允许所有。
  • Access-Control-Allow-Credentials

    • 非必须字段,一个布尔值。true: 表示当前CORS这个请求中是否允许Cookie。默认是false。
  • Access-Control-Expose-Headers

    • 非必须字段,指定允许HMLHttpRequest对象的getResponseHeader()方法获取哪些字段。如果没有默认能够获取如下字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。

关于是否允许传递Cookie不只是服务器将字段Access-Control-Allow-Credentials设置为true,需要在AJAX中打开withCredentials属性。否则浏览器也不会发送Cookie。

注意:如果需要发送Cookie,那么Access-Control-Allow-Origin字段就不能设置为*,且原网页代码中的document.cookie也无法读取服务器域名下的Cookie

非简单请求

非简单请求是对服务器有特殊要求的请求,比如:请求方法是PUT或DELETE,或Content-Type字段的类型是application/json

非简单请求的CORS,会在正式通信之前,增加一次HTTP查询请求,称为‘预检’请求(preflight)。

浏览器会先询问服务器,当前网页所在的域名是否在服务器许可的名单之中,以及可以使用哪些HTTP动词和头信息字段。得到肯定之后浏览器才会发出XMLHttpRequest请求,否则就报错。

比如下面这段请求:

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

请求方法是PUT,头加入了X-Custom-Header信息,浏览器发现这是一个非简单请求,就会发起一个预检请求,要求服务器确认是否可以这样请求。

预检请求头部信息:

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

预检请求用的方法是OPTIONS,表示这个请求是用来询问的。除了Origin字段,预检请求头的信息包含两个特殊字段:

  • Access-Control-Request-Method

    • 列出浏览器CORS请求会用到哪些HTTP方法,比如上面的PUT
  • Access-Control-Request-Headers

    • 是一个逗号分隔的字符串,表示额外发送的头信息字段。

预检请求的回应: 当预检请求结束之后,就可以做出回应了。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

回应里面的字段Access-Control-Allow-Origin表示当前的源可以请求数据,服务器回应的其他关于CORS的相关字段:

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
  • Access-Control-Allow-Methods

    • 必须字段,值是逗号分隔的字符串,表示服务器所支持的跨源请求方法。目的是为了避免多次预检请求。
  • Access-Control-Allow-Headers

    • 如果浏览器请求头中包含Access-Control-Request-Headers字段,那这个字段对于服务器来说就是必须的。值是逗号分隔的字符串,表示服务器支持的所有头信息字段,不限于浏览器在预检中请求的字段。
  • Access-Control-Allow-Credentials

    • 此字段与请求时的含义是相同的,与Cookie有关。
  • Access-Control-Max-Age

    • 非必须字段。表示预检请求的有效期限,单位为秒。即,允许缓存该条回应为多少秒,在有效期限内不需要再发送预检请求。

预检请求之后,浏览器正常的CORS请求头:

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器的响应是:

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

字段Access-Control-Allow-Origin是每次响应都必须携带的。

总结

本篇文章主要讲解如下:

  • XMLHttpRequest是AJAX请求封装的根本。
  • AJAX部分请求不能跨域的原因是因为同源策略,这个策略是浏览器的。
  • 解决跨域请求问题除了部分标签以外,还有CORS。
  • CORS的原理,以及工作细节。