如何避免单页应用程序中的 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-Methods
和Access-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 预检请求?的主要内容,如果未能解决你的问题,请参考以下文章
ASP.NET Core:带有 OPTIONS 异常的 Windows 身份验证(CORS 预检)
使用 AWS Cloudfront 避免 CORS,并清理 SPA url