如何避免单页应用程序中的 CORS 预检请求?

Posted

技术标签:

【中文标题】如何避免单页应用程序中的 CORS 预检请求?【英文标题】:How to avoid CORS preflight requests in Single Page Applications? 【发布时间】:2020-08-09 03:23:13 【问题描述】:

我想构建一个单页应用程序 (SPA),并在 分离后端 API (REST) 和前端资产(静态 vue.js 代码)时注意到以下问题:

从 API 后端以外的其他域提供 index.html 时,大多数 POST/PUT 请求都会触发 CORS 预检请求。

我做了一些研究,发现博客文章 [1][2] 讨论了这个问题 - 没有给出实际的解决方案。 某些标头(例如 Authorization 标头和具有 application/json 值的 Content-Type 标头)不允许作为 cors-safelisted-request-header。因此 POST/PUT 请求会触发 CORS 预检请求。这是不可接受的,因为它会增加相当多的延迟。

问题

如果两个域都属于同一实体,是否可以避免这些预检请求?

研究

我对如何避免前端和后端之间的 CORS 请求进行了一些研究。该解决方案需要从与 REST API 后端相同的域提供 index.html 文件(请参见下面的示例)。我想知道不使用单独的域是否是避免 SPA 的 CORS 请求的唯一解决方案。

场景(示例)

单页应用程序(SPA);前后端层 托管在 AWS 云中 第 1 层:具有 S3 存储桶源的 CloudFront CDN - 在 static.example.com 上提供静态资产(Vue.js 前端) 第 2 层:具有 ECS 集成的负载平衡器,它运行 node.js 容器以在 example.com 上托管 (REST) 后端 第 1 层和第 2 层之间的通信使用 HTTPS 协议和 REST 范例。 index.html 由第 2 层提供服务,客户使用 example.com 打开 web 应用程序。 index.html 通过指向同一个域 (example.com) 来引用 API。它通过指向 CDN (static.example.com) 来引用静态 vue 资产。 SPA 由两部分组成:a) 公共资产(.js 文件、.css 文件等)和 b) index.html 文件。后者由同样托管 REST API 的后端容器群提供服务。

参考文献

[1]https://www.freecodecamp.org/news/the-terrible-performance-cost-of-cors-api-on-the-single-page-application-spa-6fcf71e50147/ [2]https://developer.akamai.com/blog/2015/08/17/solving-options-performance-issue-spas

【问题讨论】:

【参考方案1】:

如何避免单页应用程序中的 CORS 预检请求?

首先,您不能“避免”某些属于标准的内容。其次,这个问题被错误地陈述了,因为 SPA 本身可以使用或不使用 CORS。这完全取决于您的设置/设计。如果您想避免预检,请不要从其他来源请求资源。

如果两个域都在,是否可以避免这些预检请求? 属于同一实体?

没有。

网站所有权与 CORS 无关。跨域资源共享意味着你想在不同的origins之间共享资源。不一定在不同的域之间。起源不是实体也不是所有者。来源只是 URL 的一部分。

最好的解决方案是完全避免引入 CORS 问题并始终坚持同源。您认为应该将后端 API 与具有不同来源的前端资产分开的假设是不正确的。

在您的设置中引入多个来源和域会增加比它解决的问题更多的问题。理想情况下,您的整个设置应该对连接的客户端隐藏,并减少到单个域和来源。

你想要的设置

                     -> static file server
CDN -> Load balancer -> api server
                     -> api server
                     -> ...

示例配置

使用您的负载平衡器及其 ACL 规则。告诉它把所有流量路由到它应该去的地方。我将以haproxy 为例,因为这是我使用的,而且据我所知,这在软件负载平衡解决方案方面几乎是行业标准。

这不是整个配置,只是与路由流量相关的部分。

# part of haproxy configuration file, usually located at /etc/haproxy/haproxy.cfg

frontend http-in # this is where requests get in to load balancer
  bind *:80
  acl data path_beg /api  # "catch" any request with path beginning with "/api"
  use_backend api if data # then route it to api backend defined below
  default_backend static  # any non-matching request we direct to static file server

backend static
  server node1 127.0.0.1:3000 # server hosting static files (index.html)

backend api
  server node1 127.0.0.1:4000 # application servers
  server node2 ...

【讨论】:

您的回答确实解决了问题!我还想在负载均衡器级别上以不同的方式路由流量 - 不使用两个不同的域并引入 CORS 问题。但是,使用 AWS 托管的 Application Load Balancer 并不容易。话虽如此,谢谢您的出色回答! 我同意这个问题的措辞不是很精确,事实上,要求解决另一个问题......但是正如 mbuechmann 上面回答的那样,有可能(在一定程度上)防止通过不使用特定标头来发送预检请求。感谢您的回答,我现在明白了,这与我试图解决的架构问题无关,但我想以某种方式重新表述这个问题,以便至少为 SO 社区提供任何好处。 @MartinLöper 如果您想使用 AWS 的 ELB,“应用程序负载均衡器”可能适合该任务。当我在这里查看aws.amazon.com/elasticloadbalancing/features/?nc=sn&loc=2 时,它说它支持第 7 层上的基于路径的路由,因此它可能会模仿我上面描述的行为。不确定,因为我使用 AWS 的次数不多。 @MartinLöper 我刚刚意识到您已经提到了“应用程序负载均衡器”。也许亚马逊支持可以帮助设置呢?如果这种设置是可能的,我会与他们确认,或者可能使用他们阵容中的不同产品(带有 haproxy 的单独盒子),或者考虑更换提供商。 我刚刚发现了一个关于 serverfault 的问题,它也在讨论这个问题。 One answer 建议在 CDN 级别而不是在 ALB 上进行配置。我会尝试两者,但我认为 ALB 不支持它。我想坚持使用 AWS 托管负载平衡(因为它可以工作并且开箱即用),这意味着我可能必须在 CDN 级别对其进行配置。【参考方案2】:

那里没有太多选择。

最简单的解决方案是从与您的 API 相同的域交付 html 和资产。

第二个选项是仅使用 cors-safelisted-request-header 的标头。

我注意到了什么:

Content-Type 标头可以替换为 Accept 标头。这个头没问题。

如果您正在执行 XHR 请求,您可以省略 Authentication 标头,而是通过将 XHR 请求的 withCredentials 字段设置为 true 来自动添加身份验证信息。 VanillaJS 示例:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.example.org/api/whatever', true);
xhr.withCredentials = true;
xhr.send();

如果您使用任何其他 XHR 客户端,请查阅文档是否可以设置该选项。

另一种选择是使用 cookie 和服务器端会话进行身份验证。当您使用 AWS 时,AWS Cognito 可能是一种选择。

如果使用的更多标头不是安全列出的 CORS 标头,则必须删除它们。

【讨论】:

设置 withCredentials 是否意味着必须通过 cookie 提供授权,而不是使用 Authorization 标头? 我查了一下,我写得比 MDN 更好:The XMLHttpRequest.withCredentials property is a Boolean that indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests. 请参阅:developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/… Cookie 似乎是安全的选择。不过,您仍然需要获得 API 域的授权。【参考方案3】:

Access-Control-Max-Age 可能会有所帮助:

Access-Control-Max-Age 响应标头指示预检请求的结果(即包含在 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 标头中的信息)可以缓存多长时间。

...实际上您发布的link 提到了它:

你可能会说是的。我们可以使用 Access-Control-Max-Age 标头来缓存预检请求的结果。

然后他们继续警告:

预检缓存的工作方式是每个 URL,而不仅仅是源。这意味着路径中的任何更改(包括查询参数)都需要另一个预检请求。

但您可以继续使用相同的 URL 并在请求正文中发送所有参数。

还有

如果两个域都属于同一实体,是否可以避免这些预检请求?

没有。浏览器没有实用的方法来检查域的所有权。有时即使对人类来说,这也不是一件容易的事。

【讨论】:

【参考方案4】:

据我了解,您希望避免 CORS 限制。为此,您必须修改网站的 web.config 文件以允许 CORS,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <system.webServer>
   <httpProtocol>
     <customHeaders>
       <add name="Access-Control-Allow-Origin" value="*" />
     </customHeaders>
   </httpProtocol>
 </system.webServer>
</configuration> 

我认为可能有办法以编程方式做到这一点,但这对我来说是有效的。 您可能仍会在浏览器控制台中收到一些 CORS 警告或错误,但除此之外它仍然有效。

【讨论】:

问题是关于 CORS 预检请求而不是在服务器端启用 CORS。后者已经配置成功;)

以上是关于如何避免单页应用程序中的 CORS 预检请求?的主要内容,如果未能解决你的问题,请参考以下文章

如何避免文件上传时的 CORS 预检请求?

使用 CORS 避免预检 OPTIONS 请求

ASP.NET Core:带有 OPTIONS 异常的 Windows 身份验证(CORS 预检)

使用 AWS Cloudfront 避免 CORS,并清理 SPA url

如何在 Wildfly 上使用 Angular 7 单页应用程序避免 404 错误

为啥浏览器允许设置一些没有 CORS 的标头,但不允许设置其他标头?试图避免预检