【问题标题】:How to avoid CORS preflight requests in Single Page Applications?如何避免单页应用程序中的 CORS 预检请求?
【发布时间】: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

【问题讨论】:

    标签: amazon-web-services architecture cors single-page-application


    【解决方案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。后者已经配置成功;)
      猜你喜欢
      • 2015-08-04
      • 2015-09-01
      • 2014-03-10
      • 2015-11-26
      • 1970-01-01
      • 2020-07-23
      • 1970-01-01
      • 2021-02-18
      • 1970-01-01
      相关资源
      最近更新 更多