深入理解 CORS (跨域资源共享)

这篇文章将会带你深入了解 CORS (Cross-Origin Resource Sharing) 的概念和设置方法,确保您的网站能在遵守同源政策的前提下正确处理跨域请求。学习如何设置 Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Allow-Origin 等 header,以及在使用 cookie 时的额外设置。

同源政策 (Same-Origin Policy)

首先我们来认识浏览器的「同源政策」。

大家应该都有用过浏览器提供的 fetch API 或 XMLHttpRequest 等方式,让我们通过 JavaScript 获取资源。常见的应用是向后端 API 获取数据再呈现在前端。

需要注意的是,用 JavaScript 通过 fetch API 或 XMLHttpRequest 等方式发起请求,必须遵守同源政策 (same-origin policy)。

什么是同源政策呢?简单地说,用 JavaScript 访问资源时,如果是同源的情况下,访问不会受到限制;

然而,在同源政策下,非同源的请求则会因为安全性的考量受到限制。浏览器会强制你遵守 CORS (Cross-Origin Resource Sharing,跨域资源访问) 的规范,否则浏览器会让请求失败。

什么是同源?

那什么情况是同源,什么情况不是呢?所谓的同源,必须满足以下三个条件:

  1. 相同的通讯协议,即 http/https

  2. 相同的域名

  3. 相同的端口

举例:下列哪个与 example.com/a.html 为同源?

跨域请求

不是同源的情况下,就会产生一个跨域 http 请求(cross-origin http请求)。

举个例子,例如我想要在 bugbug.io 的页面上显示来自 othersite.com 的数据,于是我利用浏览器的 fetch API 发送一个请求:

javascript 复制代码
try {
  fetch('https://othersite.com/data')
} catch (err) {
  console.error(err);
}

这时候就产生了一个跨域请求。而跨域请求必须遵守 CORS 的规范。

当服务器没有正确设置时,请求就会因为违反 CORS 而失败,在 Chrome DevTool 就会看到以下的经典错误:

csharp 复制代码
Access to fetch at *** from origin *** has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

我们接下来就一起来看 CORS 到底是什么,又该如何正确地设置 CORS 吧!

什么是 CORS (Cross-Origin Resource Sharing)?

终于要进入重点了,到底什么是 CORS?

简单地说,CORS (Cross-Origin Resource Sharing) 是针对不同源的请求而定的规范,通过 JavaScript 访问非同源资源时,后端服务必须明确告知浏览器允许何种请求,只有后端服务允许的请求才能够被浏览器实际发送出去,否则就会失败。

在 CORS 的规范里面,跨域请求有分两种:「简单」的请求和非「简单」的请求。

接下来会分别解释两种请求的 CORS 分别如何运行的。

简单跨域请求

所谓的「简单」请求,必须符合下面两个条件:

  1. 只能是 HTTP GET, POST or HEAD 方法

  2. 自定义的请求header 只能是 AcceptAccept-LanguageContent-LanguageContent-Type(值只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain)。细节可以看 fetch spec。

不符合以上任一条件的请求就是非简单请求。

举个例子来说,下面这个请求不是一个简单的请求:

php 复制代码
const response = await fetch('https://othersite.com/data', {
  method: 'DELETE',
  headers: {
    'Content-Type': 'application/json',
    'X-CUSTOM-HEADER': '123'
  }
});

违反简单请求的地方有三个,分别是:

  1. 他是 HTTP DELETE 方法;

  2. 他的 Content-Type 是 application/json;

  3. 他带了不合规范的 X-CUSTOM-HEADER。

Origin (来源)

首先,浏览器发送跨域请求时,会带一个 Origin header,表示这个请求的来源。

Origin 包含通讯协议、域名和端口三个部分。

所以从 https://bugbug.io 发出的往 https://othersite.com/data 的请求会像这样:

vbnet 复制代码
GET /data/
Host: othersite.com
Origin: https://shubo.io
...

Access-Control-Allow-Origin

当后端服务端收到这个跨域请求时,它可以依据「请求的来源」,即 Origin 的值,决定是否要允许这个跨域请求。如果后端服务允许这个跨域请求,它可以「授权」给这个来源的 JavaScript 访问这个资源。

授权的方法是在 response 里加上 Access-Control-Allow-Origin header:

arduino 复制代码
Access-Control-Allow-Origin: https://bugbug.io

如果后端服务允许任何来源的跨域请求,那可以直接回 *

makefile 复制代码
Access-Control-Allow-Origin: *

当浏览器收到响应时,会检查请求中的 Origin header 是否符合响应的 Access-Control-Allow-Origin header,相符的情况下浏览器就会让这个请求成功,我们也可以顺利地用 JavaScript 读取到响应;反之,则浏览器会将这个请求视为是不安全的而让它失败,即便后端服务确实收到请求也成功地响应了,但基于安全性的原因 JavaScript 中也没有办法读到响应。

JavaScript 默认可以访问的「简单」response header 有以下这些:

  • Cache-Control

  • Content-Language

  • Content-Type

  • Expires

  • Last-Modified

  • Pragma

如果要让 JavaScript 访问其他 header,后端服务端可以用 Access-Control-Expose-Headers header 设置。

vbnet 复制代码
X-MY-CUSTOM-HEADER: 123
X-MY-OTHER-CUSTOM-HEADER: 123
Access-Control-Expose-Headers: X-MY-CUSTOM-HEADER, X-MY-OTHER-CUSTOM-HEADER

一般跨域请求

非「简单」的跨域请求,例如:HTTP PUT/DELETE 方法,或是 Content-Type: application/json 等,浏览器在发送请求之前会先发送一个 「preflight请求(预检请求)」,其作用在于先询问服务器:你是否允许这样的请求?真的允许的话,我才会把请求完整发送过去。

Preflight Request (预检请求)

什么是 preflight请求呢?

Preflight 请求是一个 HTTP OPTIONS 方法,会带有两个请求 header:Access-Control-Request-MethodAccess-Control-Request-Headers

  • Access-Control-Request-Method: 非「简单」跨域请求的 HTTP 方法。

  • Access-Control-Request-Headers 非「简单」跨域请求带有的非「简单」header。

比方说我发送的非「简单」跨域请求是这样:

php 复制代码
fetch('https://othersite.com/data/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CUSTOM-HEADER': '123'
  }
})

那我的请求header 会长得会像这样:

makefile 复制代码
POST /data/
Host: othersite.com
Origin: https://bugbug.io
Content-Type: application/json
X-MY-CUSTOM-HEADER: 123

浏览器帮我们发送的 preflight请求就会像这样:

makefile 复制代码
OPTIONS /data/
Host: othersite.com
Origin: https://bugbug.io
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-MY-CUSTOM-HEADER, Content-Type

Preflight Response

那收到 preflight请求时,后端服务该做什么呢?

后端服务必须告诉浏览器:我允许的方法和 header 有哪些。因此 后端服务的响应必须带有以下两个 header:

  • Access-Control-Allow-Methods: 允许的 HTTP 方法。

  • Access-Control-Allow-Headers: 允许的非「简单」header。

当浏览器看到跨域请求的方法和 header 都有被列在允许的方法和 header 中,就表示可以实际发送请求了!

以上面提到例子来说,如果后端服务可以接受上述的请求,后端服务的 preflight response 应该要像这样:

makefile 复制代码
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: X-MY-CUSTOM-HEADER, Content-Type

浏览器收到正确的 preflight response,表示 CORS 的验证通过,就可以送出跨域请求了!

接下来,浏览器实际帮我们送出以下的跨域请求:

makefile 复制代码
POST /data/
Host: othersite.com
Origin: https://bugbug.io
Content-Type: application/json
X-MY-CUSTOM-HEADER: 123

最后一步,后端服务还是要响应 Access-Control-Allow-Origin header。浏览器会再检查一次跨域请求的响应是否带有正确的 Access-Control-Allow-Origin header:

arduino 复制代码
Access-Control-Allow-Origin: https://bugbug.io

这一步也检查无误的话,我们的跨域请求才算正式成功喔!这时候我们才能在 JavaScript 中读取响应的内容:

javascript 复制代码
fetch('https://othersite.com/data/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CUSTOM-HEADER': '123'
  }
})
.then(response => response.json())
.then(json => {
  console.log(json);
});

一般的 HTTP 请求会带有该域名底下的 cookie;然而,跨域请求默认是不能带 cookie 的。

为什么呢?因为带有 cookie 的请求非常强大,如果请求携带的 cookie 是 session token,那这个请求可以以你的身份做很多危险的事情,像是访问你的隐私数据、从你的银行帐户转帐等。所以浏览器端针对跨域请求的 cookie 也做了规范。

首先,请求必须要明确地标示「我要访问跨域 cookie」。使用 fetch API 和 XMLHttpRequest 的设置方法如下:

  • credentials

通过 fetch API 发送跨域请求,需要设置 credentials: 'include'

php 复制代码
fetch('https://othersite.com/data', {
  credentials: 'include'
})
  • withCredentials

通过 XMLHttpRequest 发送跨域请求,需要设置 withCredentials = true;

ini 复制代码
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('POST', 'https://othersite.com/data');

如此一来跨域请求就会携带 cookie 了!

后端服务也需要额外的设置:如果是信任的来源,响应要带有 Access-Control-Allow-Credentials header:

arduino 复制代码
Access-Control-Allow-Credentials: true

如此一来,浏览器才会将 cookie 写进该域名。

注意:如果是允许使用 cookie 的情况,Access-Control-Allow-Origin 不能用 *,必须明确标示哪些来源允许访问。 理由也是基于安全性考量,因为可以用 cookie 的情况下,通常表示会访问一些比较私人的数据,假设任何网站都能够访问这样的数据,显然是有点危险的!所以不能设为 *!

如果你偷懒地用了 Access-Control-Allow-Origin: *,就会无情地收到来自浏览器的错误:

less 复制代码
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when therequest's credentials mode is 'include'. Origin http://localhost:8080 is therefore not allowed access. Thecredentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

总结

遇到 CORS 的问题,可以归纳出这样的最佳实践:

  1. 先弄清楚是否为「简单」的跨域请求,如果是,在后端 GET/POST/HEAD 方法本身加上 Access-Control-Allow-Origin header。

  2. 如果非「简单」跨域请求,在后端 OPTIONS 加上 Access-Control-Allow-MethodsAccess-Control-Allow-Headers header。另外,在后端方法本身加上 Access-Control-Allow-Origin header。

  3. (Optional) 需要使用 cookie 的情况下,前端要加上 credentials: 'include' 或是 withCredentials 参数,后端要加上 Access-Control-Allow-Credentials header,而且 Access-Control-Allow-Origin header 不能用 *

相关推荐
GISer_Jing36 分钟前
Node.js学习路线
前端·javascript·node.js
Wang's Blog2 小时前
Webpack: 持久化缓存大幅提升构建性能
前端·缓存·webpack
程序员储物箱2 小时前
Vue报错:Module not found: Error: Can‘t resolve ‘less-loader‘ in ‘文件地址‘
前端·vue
乐吾乐科技2 小时前
【国产开源可视化引擎Meta2d.js】锚点
前端·编辑器·web·数据可视化·大屏端
Her...2 小时前
electron教程(一)创建项目
前端·javascript·electron
logstach3 小时前
QML-Grid和OpacityMask
前端·qml
C+ 安口木4 小时前
前端代码规范 - 日志打印规范
前端·代码规范
朝阳394 小时前
js【最佳实践】遍历数组的八种方法(含数组遍历 API 的对比)for,forEach,for of,map,filter,reduce,every,some
javascript
Jesse_Kyrie4 小时前
配置windows环境下独立浏览器爬虫方案【不依赖系统环境与chrome】
前端·chrome·爬虫·python·scrapy
詩筠5 小时前
SpringBoot实战:轻松实现XSS攻击防御(注解和过滤器)
java·spring boot·后端·xss