本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
本文打算重新学习一下关于跨域的问题。本文会加入自己的理解,对于十分了解的朋友,可以略过。
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的原理,以及工作细节。