Node.js 后端开发指南:搭建、优化与部署
REST API 的 CORS(跨源资源共享)权威指南
有关 REST API 跨域资源共享 (CORS) 的深入指南,包括 CORS 的工作原理以及常见陷阱(尤其是在安全性方面)。
什么是 CORS?
CORS 是一种安全机制,允许来自一个域或源的网页访问具有不同域的资源(跨域请求)。 CORS 是现代浏览器中实现的同源策略的放宽。如果没有 CORS 之类的功能,网站就只能通过所谓的同源策略访问来自同一源的资源。
为什么存在同源?
和许多网站一样,您可能会使用 Cookie 来跟踪身份验证或会话信息。这些 Cookie 在创建时会绑定到某个域。每次对该域进行 HTTP 调用时,浏览器都会附加为该域创建的 Cookie。每次HTTP 调用都是如此,可能是针对静态图像、HTML 页面,甚至是 AJAX 调用。
这意味着当您登录https://examplebank.com时,会为https://examplebank.com存储一个 cookie 。如果该银行是单页 React 应用程序,他们可能已在https://examplebank.com/api创建 REST API ,以便 SPA 通过 AJAX 进行通信。
跨域漏洞
假设你 登录https://examplebank.com后浏览到一个恶意网站https://evilunicorns.com。如果没有同源策略,那么黑客网站可以对https://examplebank.com/api进行经过身份验证的恶意 AJAX 调用,即使黑客网站无法直接访问银行的 cookie。POST /withdraw
这是因为浏览器会自动将与https://examplebank.com绑定的任何 cookie 附加到该域的任何 HTTP 调用,包括从https://evilunicorns.com到https://examplebank.com 的AJAX 调用。通过将 HTTP 调用限制为仅来自同一来源(即浏览器选项卡的域)的调用,同源策略可以关闭一些黑客后门,例如跨站点请求伪造(CSRF)(尽管不是全部。CSRF 令牌等机制仍然是必要的)。
Origin 是如何定义的?
来源包括协议、域和端口的组合。这意味着https:// api .mydomain.com和https://mydomain.com实际上是不同的来源,因此受到同源策略的影响。类似地,http://localhost: 9000和http://localhost: 8080也是不同的来源。考虑来源时会忽略路径或查询参数。
来源是指发起请求的内容,通常是打开的浏览器选项卡,但也可能是 iFrame 窗口的来源。
为什么要创建 CORS?
网站发出跨域 HTTP 请求是有正当理由的。也许https://mydomain.com上的单页应用需要对https://api.mydomain.com进行 AJAX 调用;或者也许https://mydomain.com包含一些第三方字体或分析提供商,如 Google Analytics 或 MixPanel。 跨域资源共享(CORS) 支持这些跨域请求。
CORS 如何工作?
CORS 请求有两种类型:简单请求和预检请求。关于请求是否预检的规则将在后面讨论。
一、简单请求
简单请求是在发起之前不需要预检请求(初步检查)的 CORS 请求。
- 打开浏览器选项卡以
https://www.mydomain.com
发起 AJAX 请求GET https://api.mydomain.com/widgets
- 除了添加类似的标头之外
Host
,浏览器还会自动添加Origin
跨源请求的请求标头:
GET /widgets/ HTTP/1.1
Host: api.mydomain.com
Origin: https://www.mydomain.com
[Rest of request...]
- 服务器检查
Origin
请求标头。如果 Origin 值被允许,则将 设置Access-Control-Allow-Origin
为请求标头中的值Origin
。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mydomain.com
Content-Type: application/json
[Rest of response...]
- 当浏览器收到响应时,浏览器会检查标头以查看其是否与选项卡的来源匹配。如果不匹配,则阻止响应。如果与单一来源完全匹配或包含通配符*运算符,
Access-Control-Allow-Origin
则检查通过(如本例所示) 。Access-Control-Allow-Origin
响应的服务器Access-Control-Allow-Origin: *
允许所有来源,这可能带来很大的安全风险。
仅当您的应用程序绝对需要它(例如创建开放/公共 API)时才使用 *。
意图
可以看到,服务器可以根据请求的来源来决定是否允许该请求。浏览器保证Origin
请求标头的设置可靠且准确。
II. 预先检查的请求
预检请求是另一种类型的 CORS 请求。预检请求是一种 CORS 请求,其中浏览器需要在发送预检请求之前发送预检请求(即初步探测),以询问服务器是否可以继续执行原始 CORS 请求。此预检请求本身是OPTIONS
对同一 URL 的请求。
由于原始 CORS 请求之前有一个预检请求,因此我们将原始 CORS 请求称为已预检。
如果出现以下情况,则任何 CORS 请求都必须进行预检:
- 它使用除 GET、HEAD 或 POST 之外的方法。此外,如果使用 POST 发送 Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 的请求数据(例如,如果 POST 请求使用 application/xml 或 text/xml 向服务器发送 XML 负载),则该请求已进行预检。
- 它在请求中设置自定义标头(例如,请求使用诸如 X-PINGOTHER 之类的标头)
来源: Mozilla
需要预检请求的典型情况:
- 一个网站通过 AJAX 调用 POST JSON 数据到 REST API,这意味着
Content-Type
标头是application/json
- 网站对 API 进行 AJAX 调用,使用令牌在请求标头中对 API 进行身份验证,例如
Authorization
这意味着,支持单页应用程序的 REST API 通常可以对大多数 AJAX 请求进行预检。
示例流程
- 打开浏览器选项卡,
https://www.mydomain.com
启动一个POST https://api.mydomain.com/widgets
带有 JSON 有效负载的经过身份验证的 AJAX 请求。浏览器OPTIONS
首先发送请求(又称为预检请求),其中包含主请求的建议请求方法和请求标头:
OPTIONS /widgets/ HTTP/1.1
Host: api.mydomain.com
Origin: https://www.mydomain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
[Rest of request...]
- 服务器响应并指定允许的 HTTP 方法和标头。如果原始 CORS 请求旨在发送列表中不存在的标头或 HTTP 方法,则浏览器将失败,而不会尝试 CORS 请求。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mydomain.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Content-Type: application/json
[Rest of response...]
- 由于标头和方法通过了检查,浏览器将发送原始 CORS 请求。请注意,
Origin
此请求中也包含标头。
POST /widgets/ HTTP/1.1
Host: api.mydomain.com
Authorization: 1234567
Content-Type: application/json
Origin: https://www.mydomain.com
[Rest of request...]
- 响应在
Access-Control-Allow-Origin
标头中具有正确的来源,因此检查通过并将控制权交还给浏览器选项卡。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mydomain.com
Content-Type: application/json
[Rest of response...]
高级配置
您已经在前面的示例中看到了一些用于 CORS 的标头,例如Access-Control-Allow-Origin
和Access-Control-Allow-Methods
,但还有更多标头可用于更精细的控制。以下是控制 CORS 的标头的完整列表。
请求标头
标头名称 | 示例值 | 描述 | 用于预检请求 | 用于 CORS 请求 |
---|---|---|---|---|
起源 | https://www.mydomain.com | 打开的浏览器选项卡的协议、域和端口组合 | 是的 | 是的 |
访问控制请求方法 | 邮政 | 对于预检请求,指定原始 CORS 请求将使用的方法 | 是的 | 不 |
访问控制请求标头 | 授权,X-PING | 对于预检请求,逗号分隔的列表指定原始 CORS 请求将发送的标头 | 是的 | 不 |
响应标头
当前浏览器尚未完全实现Access-Control-Allowed-Headers、Access-Control-Allow-Methods 和 Access-Control-Expose-Headers上的通配符 (*) 。
最好列出标头或方法。
常见陷阱
1. 对 Access-Control-Allow-Origin 使用 * 运算符。
CORS 是在尝试保持安全性的同时放宽同源策略的一种方式。使用 * 会禁用 CORS 的大多数安全规则。有些情况下可以使用通配符,例如集成到许多第三方网站的开放 API。
您可以通过将 API 放在不同的域上来提高安全性。
例如,您的开放 API https://api.mydomain.com可以响应Access-Control-Allow-Origin: *
,但您主网站的 API https://www.mydomain.com/api仍然响应Access-Control-Allow-Origin: https://www.mydomain.com
2. 为 Access-Control-Allow-Origin 返回多个域。
不幸的是,规范不允许Access-Control-Allow-Origin: https://mydomain.com, https://www.mydomain.com
。服务器只能使用一个域或 * 进行响应,但您可以利用Origin
请求标头。
3. 使用通配符选择,如*.mydomain.com。
这不是 CORS 规范的一部分,通配符只能用于表示允许所有域。
4. 不包括协议或非标准端口。
Access-Control-Allow-Origin: mydomain.com
由于未包含协议,因此无效。
类似地,Access-Control-Allow-Origin: http://localhost
除非服务器实际上在标准 HTTP 端口上运行,否则您将遇到麻烦:80
。
5. Vary 响应标头中未包含 Origin
大多数 CORS 框架都会自动执行此操作,您必须向客户端指定服务器响应将根据请求来源而有所不同。
6.未指定 Access-Control-Expose-Headers
如果未包含必需的标头,CORS 请求仍将通过,但未列入白名单的响应标头将在浏览器选项卡中隐藏。CORS 请求始终公开的默认响应标头是:
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
7.当 Access-Control-Allow-Credentials 设置为true时使用通配符
这是一个让很多人困惑的棘手案例。如果响应包含Access-Control-Allow-Credentials: true
,则通配符运算符不能用于任何响应标头,例如 Access-Control-Allow-Origin。
文章来源:Authoritative guide to CORS (Cross-Origin Resource Sharing) for REST APIs