简介
跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种机制,该机制使用附加的 HTTP 头来告诉浏览器,准许运行在一个源上的Web应用访问位于另一不同源选定的资源。 当一个Web应用发起一个与自身所在源(域,协议和端口)不同的HTTP请求时,它发起的即跨源HTTP请求。
出于安全性,浏览器限制脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。
注意,由于是浏览器判断响应头,因此,服务器实际上已经收到了请求。只不过返回值被浏览器拦截了
实现方式
跨源资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
触发条件
简单请求并不会触发 CORS,当同时满足以下三个条件时,即为简单请求
某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch (其中定义了 CORS)规范。若请求满足所有下述条件,则该请求可视为“简单请求”:
- 使用下列方法之一: GET HEAD POST
- 除了被用户代理自动设置的首部字段(例如 Connection ,User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为: Accept Accept-Language Content-Language Content-Type (需要注意额外的限制) DPR Downlink Save-Data Viewport-Width Width
- Content-Type 的值仅限于下列三者之一: text/plain multipart/form-data application/x-www-form-urlencoded
解决方法
- 服务器使用 Access-Control-Request-Headers 可以用来处理情况二中使用自定义 Header 的情况
- Access-Control-Allow-Methods 可以用来处理情况一
- Access-Control-Allow-Origin 用来处理域名白名单
- Access-Control-Max-Age 表明发起预检请求的缓存时间(注意浏览器本身维护了一个最大值,如果超过,则无效)
- Access-Control-Expose-Headers 用于要求浏览器携带某些 Header(否则只能拿到最基本的一些)
cookie
一般而言,跨域请求不会携带 cookie。如果需要携带 cookie,则需要在发出请求时进行特殊的设置,首先是在请求时
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
// Fetch
fetch(url, {
credentials: 'include'
})
- 请求时需要进行设置
- response header 中的 Access-Control-Allow-Credentials: true (如果为 false,则浏览器不会把相应内容返回给发送者)
- 服务器不得设置 Access-Control-Allow-Origin 的值为 *。否则请求会失败
另外,响应首部中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常
CORS 中间件处理
- 对于非OPTIONS请求的处理,要根据情况加上 Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Expose-Headers这三个响应头部;
- 对于OPTIONS请求(预检请求)的处理,要根据情况加上 Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Max-Age,Access-Control-Allow-Methods,Access-Control-Allow-Headers这几个响应头部;
验证结果
跨域 GET 请求
fetch('http://127.0.0.1:4321').then(function (response) {
if (response.ok) {
response.json().then(function (data) {
console.log(data);
});
} else {
console.log('请求失败,状态码为', response.status);
}
}, function(err) {
console.log('出错:', err);
});
跨域简单 POST 请求
fetch('http://127.0.0.1:4321/post', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'firstName=Nikhil&favColor=blue&password=easytoguess'
}).then(function(res) {
if (res.ok) {
console.log('Perfect! Your settings are saved.');
} else if (res.status == 401) {
console.log('Oops! You are not authorized.');
}
}, function(e) {
console.log('Error submitting form!');
});
失败
跨域不简单 POST 请求(JSON)
fetch('http://127.0.0.1:4321/post', {
method: 'post',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify({a: 7, str: 'Some string: &=&'})
}).then(res=>res.json())
.then(res => console.log(res));
结果:服务器连请求都收不到