JS `CORS` (跨域资源共享) 机制深度:预检请求与认证凭证

咳咳,各位前端的靓仔靓女们,早上好/下午好/晚上好!(取决于你们看这玩意儿的时间)今天咱们来聊聊 CORS 这位让人又爱又恨的兄弟。说它让人爱,是因为它保护了咱们的数据安全;说它让人恨,是因为一不小心就给你来个“CORS 错误”,让你抓耳挠腮,怀疑人生。

咱们今天就扒开 CORS 的底裤,看看它到底是怎么工作的,特别是预检请求和认证凭证这两个磨人的小妖精。

CORS:这堵墙是怎么立起来的?

想象一下,你家住在一个小区里,小区门口有个保安。这个保安的工作就是防止不该进的人进来,保护小区的安全。CORS 就像这个保安,它保护的是你的浏览器上的数据安全。

同源策略(Same-Origin Policy),是 CORS 的基石。它规定,浏览器只允许来自相同源的脚本访问另一个源的资源。那啥叫“相同源”呢?得满足以下三个条件都一样:

  • 协议(protocol): 比如 httphttps
  • 域名(host): 比如 example.com
  • 端口(port): 比如 80443

举个栗子:

URL 是否与 http://example.com/index.html 同源 备注
http://example.com/about.html 协议、域名、端口都相同
https://example.com/index.html 协议不同
http://sub.example.com/index.html 域名不同
http://example.com:8080/index.html 端口不同

如果你的网站 http://my-cool-app.com 试图从 http://another-api.com 获取数据,并且这两个 URL 不满足同源策略,那么浏览器就会发起 CORS 检查。

跨域请求:浏览器和服务器的秘密通信

当浏览器发现你要跨域请求时,它会偷偷摸摸地和服务器进行一些“交流”,这个交流过程分为两种:简单请求和预检请求。

简单请求(Simple Request)

简单请求要满足以下所有条件:

  • 请求方法是 GETHEADPOST
  • 除了浏览器自动设置的头部字段,以及以下允许手动设置的头部字段之外,没有其他自定义头部字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (但其值仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)
    • DPR
    • Downlink
    • ECT
    • RTT
    • Save-Data
    • Viewport-Width
    • Width
  • 请求中的 ReadableStream 对象未被使用。

如果请求满足以上条件,浏览器会直接发起请求,并在请求头中添加一个 Origin 字段,告诉服务器请求来自哪个源。

// 前端代码
fetch('http://another-api.com/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'text/plain'
  }
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

服务器收到请求后,会检查 Origin 字段,如果允许该源的访问,就会在响应头中添加 Access-Control-Allow-Origin 字段,指定允许访问的源。

// 服务器响应头
Access-Control-Allow-Origin: http://my-cool-app.com

如果 Access-Control-Allow-Origin 的值与请求头中的 Origin 字段相同,或者值为 *(表示允许所有源访问),那么浏览器就会认为这个跨域请求是安全的,允许脚本访问响应数据。否则,浏览器会阻止脚本访问响应数据,并抛出一个 CORS 错误。

预检请求(Preflight Request)

如果请求不满足简单请求的条件,比如使用了 PUTDELETE 等方法,或者添加了自定义头部字段,那么浏览器会先发起一个预检请求

预检请求是一个 OPTIONS 请求,它会向服务器询问该源是否允许发起实际的跨域请求。预检请求的头部字段包括:

  • Origin: 请求的源。
  • Access-Control-Request-Method: 实际请求将使用的 HTTP 方法。
  • Access-Control-Request-Headers: 实际请求将包含的自定义头部字段。
// 前端代码
fetch('http://another-api.com/data', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'foobar' // 自定义头部
  },
  body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

上面的代码中,因为使用了 PUT 方法和自定义头部字段 X-Custom-Header,所以浏览器会先发起一个预检请求。

// 预检请求
OPTIONS /data HTTP/1.1
Origin: http://my-cool-app.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, X-Custom-Header

服务器收到预检请求后,需要进行验证,并返回相应的响应头。常用的响应头包括:

  • Access-Control-Allow-Origin: 允许访问的源。
  • Access-Control-Allow-Methods: 允许使用的 HTTP 方法。
  • Access-Control-Allow-Headers: 允许使用的自定义头部字段。
  • Access-Control-Max-Age: 预检请求的缓存时间,单位是秒。
// 服务器响应头
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my-cool-app.com
Access-Control-Allow-Methods: PUT, GET, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400

如果服务器的响应头中包含了以上字段,并且值与预检请求中的字段相匹配,那么浏览器就会认为这个跨域请求是安全的,允许发起实际的请求。否则,浏览器会阻止请求,并抛出一个 CORS 错误。

Access-Control-Max-Age 字段非常重要,它告诉浏览器可以缓存预检请求的结果多长时间。在这个时间内,浏览器不会再发起预检请求,而是直接发起实际的请求。这可以减少服务器的负担,提高性能。 如果你的服务器经常变动允许的方法和头部,这个缓存时间就不能设置太长,否则会导致一些请求被错误地阻止。

认证凭证(Credentials):带着通行证去串门

有时候,我们需要在跨域请求中携带认证信息,比如 Cookie、HTTP 认证等。默认情况下,跨域请求是不携带认证信息的。如果需要携带认证信息,需要在前端和后端都进行相应的设置。

前端设置:

fetchXMLHttpRequest 中,需要设置 credentials 选项为 include

// 前端代码
fetch('http://another-api.com/data', {
  method: 'GET',
  credentials: 'include' // 允许携带认证信息
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

后端设置:

需要在响应头中添加 Access-Control-Allow-Credentials 字段,并设置为 true

// 服务器响应头
Access-Control-Allow-Origin: http://my-cool-app.com
Access-Control-Allow-Credentials: true

注意:

  • 如果 Access-Control-Allow-Credentials 设置为 true,那么 Access-Control-Allow-Origin 的值不能设置为 *,必须指定具体的源。因为允许所有源携带认证信息是非常危险的。
  • 如果浏览器检测到请求携带了认证信息,但服务器没有返回 Access-Control-Allow-Credentials: true,或者 Access-Control-Allow-Origin 的值为 *,那么浏览器会阻止脚本访问响应数据,并抛出一个 CORS 错误。

举个栗子,假设你的网站 http://my-cool-app.com 需要从 http://another-api.com 获取用户信息,并且需要携带 Cookie。

// 前端代码
fetch('http://another-api.com/user', {
  method: 'GET',
  credentials: 'include'
})
.then(response => response.json())
.then(user => console.log(user))
.catch(error => console.error('Error:', error));
// 服务器响应头
Access-Control-Allow-Origin: http://my-cool-app.com
Access-Control-Allow-Credentials: true

这样,浏览器就会在请求中携带 Cookie,并且允许脚本访问响应数据。

CORS 问题的排查与解决

CORS 错误是前端开发中常见的错误之一。当遇到 CORS 错误时,首先要检查以下几个方面:

  1. 检查请求是否跨域。 确认请求的协议、域名和端口是否与当前页面的源相同。
  2. 检查请求方法和头部字段。 如果使用了非简单请求的方法或头部字段,需要确认服务器是否支持预检请求。
  3. 检查服务器响应头。 确认服务器是否返回了正确的 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-Credentials 字段。
  4. 检查认证凭证设置。 如果需要携带认证信息,需要确认前端和后端都进行了相应的设置。

常见的解决方法:

  • 修改服务器配置。 这是最常用的方法。修改服务器配置,添加正确的 CORS 响应头。
  • 使用 JSONP。 JSONP 是一种古老的跨域解决方案,它利用 <script> 标签可以跨域请求的特性来实现跨域。但是,JSONP 只能支持 GET 请求,而且安全性较低,不推荐使用。
  • 使用代理服务器。 在同源服务器上创建一个代理接口,将跨域请求转发到目标服务器,然后将响应返回给前端。这种方法可以绕过 CORS 限制,但是会增加服务器的负担。
  • 使用 CORS 插件。 有一些浏览器插件可以自动添加 CORS 响应头,但是这种方法只适用于开发环境,不适用于生产环境。

CORS 的安全性

CORS 机制虽然可以保护数据安全,但也存在一些安全风险。例如,如果服务器的 Access-Control-Allow-Origin 设置为 *,那么所有源都可以访问该服务器的资源,这可能会导致安全问题。

因此,在配置 CORS 时,需要谨慎考虑安全因素,尽量避免使用 * 作为 Access-Control-Allow-Origin 的值,而是指定具体的源。此外,还需要对请求进行验证,防止恶意请求。

代码示例 (Node.js + Express)

// 使用 express 中间件来处理 CORS
const express = require('express');
const cors = require('cors');
const app = express();

// 配置 CORS
const corsOptions = {
  origin: 'http://my-cool-app.com', // 允许的源
  credentials: true,                 // 允许携带认证信息
  optionsSuccessStatus: 200           // 一些遗留的浏览器需要这个配置
};

app.use(cors(corsOptions));

// 处理预检请求 (OPTIONS) - express cors 中间件会自动处理,但为了演示,可以这样写
app.options('/data', cors(corsOptions)); //为特定路由启用CORS

// 模拟一个需要身份验证的 API 路由
app.get('/data', (req, res) => {
  // 假设这里有一些验证逻辑,比如检查Cookie或Authorization header
  const isAuthenticated = true; // 替换为实际的验证逻辑

  if (isAuthenticated) {
    res.json({ message: 'CORS 成功!', data: { sensitiveInfo: '这是一些敏感信息' } });
  } else {
    res.status(401).json({ message: '未授权' });
  }
});

app.listen(3001, () => {
  console.log('服务器运行在 3001 端口');
});

这个例子展示了如何在 Node.js 中使用 expresscors 中间件来处理 CORS 请求。 corsOptions 定义了允许的源和是否允许携带认证信息。 app.options('/data', cors(corsOptions)) 用于处理预检请求,虽然 cors 中间件会自动处理大部分情况,但显式声明对于理解 CORS 流程很有帮助。

总结

CORS 机制是保护 Web 应用安全的重要手段。理解 CORS 的工作原理,特别是预检请求和认证凭证,可以帮助我们更好地解决跨域问题,提高 Web 应用的安全性。 虽然一开始可能会觉得 CORS 有点烦人,但只要掌握了它的规则,就能轻松驾驭它,让你的 Web 应用更加安全可靠。记住,CORS 就像一个尽职尽责的保安,虽然有时候会让你觉得不方便,但它守护的是你的数据安全!

希望今天的讲解对大家有所帮助,如果还有什么疑问,欢迎随时提问。下次有机会再跟大家分享其他有趣的前端知识! 祝大家编程愉快,Bug 远离!

发表回复

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