http 跨域资源共享详解
由于浏览器同源策略限制,会导致出现跨域问题。而跨域资源共享(CORS
)可以突破浏览的同源策略的限制,不过需要服务端配合设置相应的响应头,从而使跨源数据传输得以安全进行。
跨域资源共享新增了一些HTTP
字段,用于与服务器相互配合。
Origin: http://foo.example
(告诉服务器来自哪个域, 浏览器自动设置,不允许手动设置)
Access-Control-Request-Method: POST
(告诉服务器使用的请求方式)
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
(告诉服务器要携带的特殊请求头字段)
而服务器也需要相应设置这些内容
Access-Control-Allow-Origin: http://foo.example
(允许这个域的请求访问服务器资源,如果为*代表允许任何域的请求访问服务器资源, 手动设置)
Access-Control-Allow-Methods: POST, GET, OPTIONS
(允许这些种类的请求方式访问服务器资源)
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
(允许请求头中可以包含这些特殊字段)
Access-Control-Max-Age: 86400
(设置预检请求的缓存时间,即针对同一个跨域的复杂请求,不必每次都先发送一次预检请求,该属性不是必须)
跨域资源共享的规范要求,对于一些可能对服务器数据产生副作用的http
方法(特别是 GET
以外的 HTTP
请求,或者搭配某些 MIME
类型 的 POST
请求),浏览器必须首先使用 OPTIONS
方法发起一个预检请求,从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的HTTP
请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证。
预检(preflight request
)
预检用来检查服务器是否支持跨域资源共享。
预检并不需要前端开发者手动发送,当需要时,浏览器会自动发送(使用OPTIONS
)
fetch 标准
对于XMLHttpRequest
来说,可以使用以下三种场景来描述跨源资源共享机制的工作原理。
工作原理
简单请求
某些请求不会触发 CORS
预检请求。在废弃的 CORS spec
中称这样的请求为简单请求,但是目前的 Fetch spec
(CORS
的现行定义规范)中不再使用这个词语。
当满足下面所有条件,这个请求就是简单请求:
- 使用请求方法为
GET
、HEAD
、POST
的其中一个 - 不得手动设置除
Accept
、Accept-Language
、Content-Language
、Content-Type
外的请求头。 Content-Type
的值只能为text/plain
、multipart/form-data
、application/x-www-form-urlencoded
的其中一个
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3000");
xhr.onreadystatechange = function () {
console.log("readyState:" + xhr.readyState);
};
xhr.send();
请求标头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 0
Host: localhost:3000
Origin: http://localhost:8080
Pragma: no-cache
Referer: http://localhost:8080/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
响应标头
Access-Control-Allow-Origin: http://localhost:8080
Connection: keep-alive
Date: Thu, 17 Nov 2022 15:10:23 GMT
Keep-Alive: timeout=5
Transfer-Encoding: chunked
预检请求
与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3000");
xhr.setRequestHeader("Content-Type", "application/json;charset=utf-8");
xhr.onreadystatechange = function () {
console.log("readyState:" + xhr.readyState);
};
xhr.send(JSON.stringify({ data: 1 }));
通过浏览器的控制台可以看到实际上发了两个请求
上面请求头的Content-Type
为application/json;charset=utf-8
,因此,该请求需要首先发起“预检请求”。
预检请求标头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET
...
实际请求标头
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
...
Access-Control-Max-Age
用于设置预检请求得缓存时间,即对于同一个跨域得复杂请求,不需要每次都发送一次预检请求。
实际的请求不会携带 Access-Control-Request-*
首部,它们仅用于 OPTIONS
请求。
附带身份凭证的请求
一般而言,对于跨域 XMLHttpRequest
或 Fetch
请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要进行额外设置(同源请求会默认带上Cookie
, 当响应中携带Cookie
,浏览器会将Cookie
进行存储,当请求时携带上去)。
当跨域时,对于XMLHttpRequest
对象,需要将实例的withCredentials
设置成true
,同时服务端也需要设置对应的响应头Access-Control-Allow-Credentials
设置成true
。
以node
为例
const app = http.createServer((req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "http://localhost:8080",
"Access-Control-Allow-Credentials": true,
"Set-Cookie": serialize("name", "leo", "utf8", { maxAge: "6000" }),
});
});