JS `CORS` 深度解析:预检请求、复杂请求与跨域安全

各位观众老爷,晚上好!我是今天的主讲人,咱们今天聊聊前端开发者避不开,但又经常觉得“头疼菊紧”的 CORS (Cross-Origin Resource Sharing) 问题。别怕,今天咱们就把它扒个精光,看看它到底是个什么玩意儿。

开场白:跨域,一个让前端夜不能寐的幽灵

作为一名Web开发者,你肯定遇到过这样的情况:你的代码明明写得天衣无缝,逻辑清晰,但浏览器却无情地甩给你一个 CORS 错误。这时候,你的内心是崩溃的,仿佛被判了死缓,而且罪名还是“跨域”。

跨域,听起来玄乎,其实说白了就是浏览器为了安全,限制了从一个源(origin)加载的文档或脚本与来自另一个源的资源进行交互。这个安全机制叫做“同源策略”(Same-Origin Policy)。

第一章:什么是同源策略?

要理解 CORS,首先得搞明白同源策略。所谓“同源”,指的是协议、域名和端口号都相同。只有这三个要素都相同,浏览器才认为两个页面来自同一个源。

举个例子:

URL 协议 域名 端口号 同源吗 (与 http://www.example.com:8080/index.html 相比)
http://www.example.com:8080/page2.html http www.example.com 8080
https://www.example.com:8080/page2.html https www.example.com 8080 否 (协议不同)
http://api.example.com:8080/page2.html http api.example.com 8080 否 (域名不同)
http://www.example.com:8081/page2.html http www.example.com 8081 否 (端口号不同)
http://www.example.com/page2.html http www.example.com 80 否 (端口号不同,默认为80)

同源策略限制了以下行为:

  • 跨域的 AJAX 请求: 不能直接使用 XMLHttpRequestfetch 从一个源向另一个源发送请求。
  • 跨域的 Cookie 访问: 一个源的 JavaScript 代码不能访问另一个源的 Cookie。
  • 跨域的 DOM 访问: 一个源的 JavaScript 代码不能直接操作另一个源的 DOM。

第二章:为什么要有同源策略?

你可能会问,为什么要有同源策略这种“反人类”的设计?它存在的意义在于安全。想象一下,如果没有同源策略,恶意网站可以轻松地窃取你的 Cookie,冒充你的身份进行操作,后果不堪设想。

举个例子:

假设你登录了银行网站 bank.example.com,浏览器会在你的电脑上保存一个 Cookie,用于验证你的身份。如果你同时访问了一个恶意网站 evil.com,如果没有同源策略,evil.com 上的 JavaScript 代码就可以直接读取 bank.example.com 的 Cookie,然后冒充你进行转账操作。

是不是想想就可怕?同源策略就是为了防止这种事情发生。

第三章:CORS:同源策略的“白名单”

同源策略虽然安全,但也带来了一些不便。很多时候,我们确实需要跨域访问资源。比如,前端应用部署在 app.example.com,后端 API 部署在 api.example.com,前端就需要跨域请求后端的数据。

这个时候,CORS 就派上用场了。CORS 是一种机制,它允许服务器告诉浏览器哪些源可以访问它的资源。简单来说,CORS 就是同源策略的“白名单”。

第四章:CORS 的工作原理

CORS 的核心在于服务器返回的 HTTP 响应头。服务器通过设置特定的响应头,来告诉浏览器是否允许跨域请求。

最关键的响应头是 Access-Control-Allow-Origin。它的值可以是一个具体的源,也可以是 *,表示允许所有源的跨域请求。

例如:

Access-Control-Allow-Origin: http://app.example.com

表示只允许 http://app.example.com 发起的跨域请求。

Access-Control-Allow-Origin: *

表示允许所有源的跨域请求。*注意:在生产环境中,除非你知道自己在做什么,否则不建议使用 ``,因为它会降低安全性。**

除了 Access-Control-Allow-Origin,还有一些其他的 CORS 响应头:

  • Access-Control-Allow-Methods: 允许的 HTTP 方法 (例如:GET, POST, PUT, DELETE)。
  • Access-Control-Allow-Headers: 允许的请求头 (例如:Content-Type, Authorization)。
  • Access-Control-Allow-Credentials: 是否允许发送 Cookie (true/false)。
  • Access-Control-Max-Age: 预检请求的缓存时间 (秒)。

第五章:简单请求 vs 复杂请求

CORS 请求分为两种:简单请求和复杂请求。浏览器对这两种请求的处理方式不同。

5.1 简单请求

简单请求是指满足以下所有条件的请求:

  • 请求方法: GET, HEAD, POST
  • 请求头: 只能包含以下字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (只限于 application/x-www-form-urlencoded, multipart/form-data, text/plain)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width

对于简单请求,浏览器会直接发送请求,并在收到响应后,检查响应头中是否包含 Access-Control-Allow-Origin,以及它的值是否与当前源匹配。如果匹配,则允许跨域访问;否则,浏览器会阻止 JavaScript 代码访问响应内容。

5.2 复杂请求

不满足简单请求条件的请求,都被认为是复杂请求。例如,使用了 PUT, DELETE 等方法,或者 Content-Type 不是 application/x-www-form-urlencoded, multipart/form-data, text/plain,或者包含了自定义的请求头。

对于复杂请求,浏览器会先发送一个“预检请求”(preflight request),也称为“OPTIONS 请求”。预检请求用于询问服务器是否允许真正的跨域请求。

第六章:预检请求 (OPTIONS 请求)

预检请求是一个 HTTP OPTIONS 请求,它包含了以下请求头:

  • Origin: 发起请求的源。
  • Access-Control-Request-Method: 实际请求使用的 HTTP 方法。
  • Access-Control-Request-Headers: 实际请求包含的自定义请求头。

服务器收到预检请求后,需要返回一个 HTTP 响应,包含以下响应头:

  • Access-Control-Allow-Origin: 允许的源。
  • Access-Control-Allow-Methods: 允许的 HTTP 方法。
  • Access-Control-Allow-Headers: 允许的请求头。
  • Access-Control-Max-Age: 预检请求的缓存时间。

只有当服务器返回的响应头表明允许跨域请求,浏览器才会发送真正的跨域请求。否则,浏览器会阻止请求。

第七章:CORS 的常见问题及解决方案

7.1 问题:No 'Access-Control-Allow-Origin' header is present on the requested resource.

这是最常见的 CORS 错误。它表示服务器没有返回 Access-Control-Allow-Origin 响应头,或者它的值与当前源不匹配。

解决方案:

  • 检查服务器配置: 确保服务器正确配置了 CORS 响应头。
  • 检查源是否匹配: 确保 Access-Control-Allow-Origin 的值与当前源匹配。
  • *使用通配符 `(不推荐):** 将Access-Control-Allow-Origin的值设置为*`,允许所有源的跨域请求。但请注意安全性。

7.2 问题:预检请求失败

预检请求失败通常是因为服务器没有正确处理 OPTIONS 请求,或者返回的 CORS 响应头不正确。

解决方案:

  • 确保服务器支持 OPTIONS 请求: 确保服务器能够正确处理 OPTIONS 请求,并返回 200 OK 状态码。
  • 检查 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 确保服务器返回的 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 包含了实际请求使用的方法和请求头。
  • 检查 Access-Control-Max-Age 可以设置 Access-Control-Max-Age 来缓存预检请求的结果,减少预检请求的次数。

7.3 问题:Cookie 没有发送

如果需要发送 Cookie,需要设置 Access-Control-Allow-Credentials: true,并且在客户端代码中设置 withCredentials = true

代码示例 (客户端):

fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include' // 或者 'same-origin'
})
.then(response => response.json())
.then(data => console.log(data));

代码示例 (服务器):

Access-Control-Allow-Origin: http://app.example.com
Access-Control-Allow-Credentials: true

注意:Access-Control-Allow-Credentials 设置为 true 时,Access-Control-Allow-Origin 的值不能设置为 *,必须是一个具体的源。

第八章:CORS 的一些“奇技淫巧”

除了标准的 CORS 机制,还有一些其他的跨域解决方案,虽然它们不属于 CORS 的范畴,但也可以在某些情况下解决跨域问题。

8.1 JSONP

JSONP (JSON with Padding) 是一种利用 <script> 标签的跨域解决方案。<script> 标签不受同源策略的限制,可以加载来自任何源的 JavaScript 代码。

JSONP 的原理是:客户端定义一个回调函数,然后将回调函数的名称作为参数传递给服务器。服务器将数据包裹在回调函数中,返回给客户端。客户端通过执行这段 JavaScript 代码,调用回调函数,获取数据。

优点:

  • 兼容性好,支持老版本的浏览器。
  • 实现简单。

缺点:

  • 只支持 GET 请求。
  • 存在安全风险,如果服务器返回的 JavaScript 代码被篡改,可能会导致安全问题。

代码示例 (客户端):

function handleData(data) {
  console.log(data);
}

let script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleData';
document.head.appendChild(script);

代码示例 (服务器):

handleData({"name": "John", "age": 30})

8.2 代理服务器

代理服务器是一种充当客户端和服务器之间中介的服务器。客户端向代理服务器发送请求,代理服务器再向目标服务器发送请求,并将响应返回给客户端。

通过使用代理服务器,可以绕过同源策略的限制。客户端向同源的代理服务器发送请求,代理服务器再向目标服务器发送请求,这样就可以实现跨域访问。

优点:

  • 可以处理各种类型的请求。
  • 不需要修改服务器端的代码。

缺点:

  • 需要搭建和维护代理服务器。
  • 可能会增加延迟。

第九章:总结与展望

CORS 是 Web 开发中一个重要的安全机制,它允许服务器控制哪些源可以访问它的资源。理解 CORS 的工作原理,以及如何正确配置 CORS 响应头,对于构建安全的 Web 应用至关重要。

虽然 CORS 看起来很复杂,但只要掌握了核心概念,就可以轻松应对各种 CORS 问题。

随着 Web 技术的不断发展,CORS 也在不断演进。未来,我们可能会看到更加灵活和安全的跨域解决方案。

最后,希望今天的分享对大家有所帮助。如果大家还有什么问题,欢迎提问!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注