【问题标题】:Cloudfront CORS issue serving fonts on Rails applicationCloudfront CORS 问题在 Rails 应用程序上提供字体
【发布时间】:2015-12-12 02:15:20
【问题描述】:

访问我的网站时,我不断从控制台收到此错误消息:

font from origin 'https://xxx.cloudfront.net' has been blocked from loading by Cross-Origin Resource Sharing policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://www.example.com' is therefore not allowed access.

我什么都试过了:

  • 我已经安装了font_assets gem
  • 配置了application.rb文件

    config.font_assets.origin = 'http://example.com'
    
  • Cloudfront 上的白名单标头,如 this article 中所述

    Access-Control-Allow-Origin
    Access-Control-Allow-Methods
    Access-Control-Allow-Headers
    Access-Control-Max-Age
    

但没什么,零,nada。

我在 Heroku 上使用 Rails 4.1。

【问题讨论】:

  • 您是否也将Origin: 标头添加到白名单?
  • 嗨迈克尔。不,我应该吗?
  • [UPDATE] 我改了,问题依旧。
  • 最好的办法是在浏览器和服务器上捕获请求和响应标头。否则你飞得有点盲目。您可能还需要对相关对象进行缓存失效,尽管更改白名单标头可能会隐式执行此操作。事实上,如果您暂时选择转发 all 标头的选项,生活可能会更轻松,这会破坏 CloudFront 的缓存部分——缓存只会使测试变得更加复杂。
  • 对不起迈克尔,但我不确定你的意思。缓存仅在生产中完成,在开发中没有问题。

标签: ruby-on-rails heroku fonts cors amazon-cloudfront


【解决方案1】:

我刚刚遇到了同样的问题并设法解决了。

您已正确告知 Cloudfront 允许这些标头,但您尚未将这些标头添加到 Cloudfront 获取字体的位置。是的,您的原始标头是允许的,但 Heroku 无论如何都不会发送带有字体的标头。

要解决此问题,您需要将正确的 CORS 标头添加到 Heroku 的字体中。幸运的是,这很容易。

首先,将rack/cors gem 添加到您的项目中。 https://github.com/cyu/rack-cors

接下来,配置您的 Rack 服务器,为它所服务的任何资产加载和配置 CORS。在 config.ru 中预加载您的应用程序后添加以下内容

require 'rack/cors'
use Rack::Cors do
  allow do
    origins '*'

    resource '/cors',
      :headers => :any,
      :methods => [:post],
      :credentials => true,
      :max_age => 0

    resource '*',
      :headers => :any,
      :methods => [:get, :post, :delete, :put, :patch, :options, :head],
      :max_age => 0
    end
  end

这会将 Heroku 返回的所有资源设置为应用正确的 CORS 标头。您可以根据您的文件和安全需求来限制标头的应用。

部署后,进入 Cloudfront 并开始对之前给您 CORS 权限错误的任何内容进行失效。现在,当 Cloudfront 从 Heroku 加载新副本时,它将具有正确的标头,并且 Cloudfront 会将这些标头传递给客户端,如之前使用您的 Origin 权限配置的那样。

为确保您从服务器提供正确的标头,您可以使用以下 curl 命令来验证您的标头: curl -I -s -X GET -H "Origin: www.yoursite.com" http://www.yoursite.dev:5000/assets/fonts/myfont.svg

您应该会看到返回的以下标头:

Access-Control-Allow-Origin: www.yoursite.com
Access-Control-Allow-Methods: GET, POST, DELETE, PUT, PATCH, OPTIONS, HEAD
Access-Control-Max-Age: 0
Access-Control-Allow-Credentials: true

【讨论】:

    【解决方案2】:

    这是一个非常难以处理的问题,原因有两个:

    1. CloudFront 正在镜像我们的 Rails 应用程序的响应标头这一事实需要您转过头来。 CORS 协议很难理解,但现在您必须在两个层面上遵循它:浏览器和 CloudFront 之间(当我们的 Rails 应用程序将其用作 CDN 时),以及浏览器和我们的 Rails 应用程序之间(当一些恶意网站想滥用我们)。

      CORS 实际上是关于浏览器和网页想要访问的第 3 方资源之间的对话。 (在我们的用例中,这就是 CloudFront CDN,为我们的应用程序提供资产。)但是由于 CloudFront 从我们的应用程序获取其访问控制响应标头,因此我们的应用程序需要提供这些标头好像它是 CloudFront 说话,并且同时不授予可能会暴露于导致同源策略/CORS 首先被开发的滥用类型的权限。特别是,我们不应授予 * 访问我们网站上的 * 资源的权限。

    2. 我发现 太多 过时的信息 - 无休止的博客文章和 SO 线程。自从发布了许多帖子以来,CloudFront 已经显着改进了其对 CORS 的支持,尽管它仍然不完美。 (CORS 确实应该开箱即用地处理。)宝石本身已经进化。

    我的设置:Rails 4.1.15 在 Heroku 上运行,资产由 CloudFront 提供。我的应用程序在“www”上都响应 http 和 https。和区域顶点,不做任何重定向。

    我简要查看了问题中提到的 font_assets gem,但很快就放弃了它,转而使用 rack-cors,这似乎更重要。我不想简单地打开所有来源和所有路径,因为这会破坏 CORS 的点和同源策略的安全性,所以我需要能够指定我允许的少数来源。最后,我个人更喜欢通过单独的 config/initializers/*.rb 文件配置 Rails,而不是编辑标准配置文件(如 config.ruconfig/application.rb) 将所有这些放在一起,这是我的解决方案,我认为这是最好的,截至2016-04-16:

    1. 宝石文件

      gem "rack-cors"
      

      rack-cors gem 在 Rack 中间件中实现了 CORS 协议。 除了在已批准的源上设置 Access-Control-Allow-Origin 和相关标头外,它还添加了一个 Vary: Origin 响应标头,指示 CloudFront 分别缓存每个源的响应(包括响应标头)。当我们的网站可以通过多个来源(例如通过 http 和 https,以及通过“www.”和裸域)访问时,这一点至关重要

    2. config/initializers/rack-cors.rb

      ## Configure Rack CORS Middleware, so that CloudFront can serve our assets.
      ## See https://github.com/cyu/rack-cors
      
      if defined? Rack::Cors
          Rails.configuration.middleware.insert_before 0, Rack::Cors do
              allow do
                  origins %w[
                      https://example.com
                       http://example.com
                      https://www.example.com
                       http://www.example.com
                      https://example-staging.herokuapp.com
                       http://example-staging.herokuapp.com
                  ]
                  resource '/assets/*'
              end
          end
      end
      

      这告诉浏览器它可以仅代表我们的 Rails 应用程序(并且代表恶意站点.com)并且仅适用于 /assets/ 网址(对于我们的控制器,not)。换句话说,允许 CloudFront 为资产提供服务,但不要像我们必须的那样打开大门。

      注意事项:

      • 我尝试将这个 after rack-timeout 插入,而不是插入中间件链的头部。 它在 dev 上工作,但没有在 Heroku 上运行,尽管 具有相同的中间件(Honeybadger 除外)。
      • 来源列表也可以作为正则表达式完成。 小心在字符串末尾锚定模式。

        origins [
            /\Ahttps?:\/\/(www\.)?example\.com\z/,
            /\Ahttps?:\/\/example-staging\.herokuapp\.com\z/
        ]
        

        但我认为阅读文字字符串更容易。

    3. 配置 CloudFront 以将浏览器的 Origin 请求标头传递到我们的 Rails 应用程序。

      奇怪的是,CloudFront 似乎将 Origin 标头从浏览器转发到我们的 Rails 应用程序无论我们是否在此处添加它,但 CloudFront 仅在 Origin 明确时才尊重我们应用程序的 Vary: Origin 缓存指令添加到标头白名单中(截至 2016 年 4 月)。

      请求头白名单有点埋没。

      如果发行版已经存在,您可以在以下位置找到它:


      如果您尚未创建发行版,请在以下位置创建它:

      • https://console.aws.amazon.com/cloudfront/home#distributions
      • 点击创建分布

        (为了完整性和可重复性,我列出了我从默认值更改的所有设置,但白名单设置是唯一与此讨论相关的设置)

      • 传送方式:Web(非 RTMP)

      • 原点设置

        • 源域名:example.com
        • 原始 SSL 协议:仅限 TLSv1.2
        • 源协议策略:仅限 HTTPS
      • 默认缓存行为设置

        • 查看器协议策略:将 HTTP 重定向到 HTTPS
        • 转发标头:白名单
        • Whitelist Headers:选择Origin并点击Add >>
        • 自动压缩对象:是

    更改所有这些内容后,请记住,任何旧的缓存值都可能需要一段时间才能从 CloudFront 过期。您可以通过转到 CloudFront 分配的 Invalidations 选项卡并为 * 创建一个失效来显式地使缓存资产失效。

    【讨论】:

    • 请注意,如果您仍然遇到问题,请检查您是不是盲目地在本地缓存响应。我有一个 django 缓存集,它正在缓存 CORS 标头,然后 CloudFront 将其提供给错误的域
    • @Noah:这是我找到的最佳答案。非常感谢..你解决了我的问题。
    • 用 curl 测试它:curl -H "Origin: https://example.com" -I https://example.com/assets/your-font.ttf
    • 这应该被标记为正确答案。只是为了提供帮助,亚马逊显然在“基于所选请求标头的缓存”下隐藏了更多的“转发标头”选项
    • 对于 Rails 5+,我相信这是一个更简单的解决方案? stackoverflow.com/a/45685644/74980
    【解决方案3】:

    如果您在Passenger 和Heroku 上运行Rails:(如果没有,请直接跳到Noach Magedman 的答案)

    Noach Magedman 的回答对我正确设置 CloudFront 非常有用。

    我还完全按照描述安装了rack-cors,虽然它在开发中运行良好,但生产中的 CURL 命令从未返回任何 CORS 配置:

    curl -H "Origin: https://tidyme-staging.com.au" -I http://tidyme-staging.com.au/assets/31907B_4_0-588bd4e720d4008295dcfb85ef36b233ee0817d7fe23c76a3a543ebba8e7c85a.ttf
    
    HTTP/1.1 200 OK
    Connection: keep-alive
    Server: nginx/1.10.0
    Date: Wed, 03 Aug 2016 00:29:37 GMT
    Content-Type: application/x-font-ttf
    Content-Length: 316664
    Last-Modified: Fri, 22 Jul 2016 03:31:57 GMT
    Expires: Thu, 31 Dec 2037 23:55:55 GMT
    Cache-Control: max-age=315360000
    Cache-Control: public
    Accept-Ranges: bytes
    Via: 1.1 vegur
    

    请注意,我直接 ping 服务器而不通过 CDN,然后在使所有内容无效后,CDN 应该只转发服务器响应的任何内容。这里重要的一行是Server: nginx/1.10.0,表示资产由 nginx 而不是 Rails 提供服务。因此,rack-cors 配置不适用。

    对我们有用的解决方案在这里:http://monksealsoftware.com/ruby-on-rails-cors-heroku-passenger-5-0-28/

    它主要涉及克隆和修改Passenger的nginx配置文件,这并不理想,因为每次Passenger升级和模板更改时都需要维护此副本。

    ===

    摘要如下:

    导航到 Rails 项目的根文件夹并复制 nginx 配置模板

    cp $(passenger-config about resourcesdir)/templates/standalone/config.erb config/passenger_config.erb
    

    打开config/passenger_config.erb 并注释掉这一行

    <%# include_passenger_internal_template('rails_asset_pipeline.erb', 8, false) %>
    

    在上面提到的行下面添加这些配置

    ### BEGIN your own configuration options ###
    # This is a good place to put your own config
    # options. Note that your options must not
    # conflict with the ones Passenger already sets.
    # Learn more at:
    # https://www.phusionpassenger.com/library/config/standalone/intro.html#nginx-configuration-template
    
    location ~ "^/assets/.+\.(woff|eot|svg|ttf|otf).*" {
        error_page 490 = @static_asset_fonts;
        error_page 491 = @dynamic_request;
        recursive_error_pages on;
    
        if (-f $request_filename) {
            return 490;
        }
        if (!-f $request_filename) {
            return 491;
        }
    }
    
    # Rails asset pipeline support.
    location ~ "^/assets/.+-([0-9a-f]{32}|[0-9a-f]{64})\..+" {
        error_page 490 = @static_asset;
        error_page 491 = @dynamic_request;
        recursive_error_pages on;
    
        if (-f $request_filename) {
            return 490;
        }
        if (!-f $request_filename) {
            return 491;
        }
    }
    
    location @static_asset {
        gzip_static on;
        expires max;
        add_header Cache-Control public;
        add_header ETag "";
    }
    
    location @static_asset_fonts {
        gzip_static on;
        expires max;
        add_header Cache-Control public;
        add_header ETag "";
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS';
        add_header 'Access-Control-Allow-Headers' '*';
        add_header 'Access-Control-Max-Age' 3628800;
    }
    
    location @dynamic_request {
        passenger_enabled on;
    }
    
    ### END your own configuration options ###
    

    更改 Procfile 以包含此自定义配置文件

    web: bundle exec passenger start -p $PORT --max-pool-size 2 --nginx-config-template ./config/passenger_config.erb
    

    然后部署...

    ===

    如果您知道更好的解决方案,请输入 cmets。

    执行后,CURL 命令产生如下响应:

    curl -H "Origin: https://tidyme-staging.com.au" -I http://tidyme-staging.com.au/assets/31907B_4_0-588bd4e720d4008295dcfb85ef36b233ee0817d7fe23c76a3a543ebba8e7c85a.ttf
    
    HTTP/1.1 200 OK
    Connection: keep-alive
    Server: nginx/1.10.0
    Date: Wed, 03 Aug 2016 01:43:48 GMT
    Content-Type: application/x-font-ttf
    Content-Length: 316664
    Last-Modified: Fri, 22 Jul 2016 03:31:57 GMT
    Expires: Thu, 31 Dec 2037 23:55:55 GMT
    Cache-Control: max-age=315360000
    Cache-Control: public
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: GET, HEAD, OPTIONS
    Access-Control-Allow-Headers: *
    Access-Control-Max-Age: 3628800
    Accept-Ranges: bytes
    Via: 1.1 vegur
    

    【讨论】:

    • 如果你正在使用 webpacker,请使用 "^/(assets|packs)/.+-([0-9a-f]{32}|[0-9a-f]{64})\..+"...
    【解决方案4】:

    从 5.0 版开始,Rails 允许为资产设置自定义 HTTP 标头,您不必使用 rack-cors 或 font-assets gem。要为资产(包括字体)设置 Access-Control-Allow-Origin,只需将以下代码添加到 config/environments/production.rb:

    config.public_file_server.headers = {
      'Access-Control-Allow-Origin' => '*'
    }
    

    标头值也可以是特定域,如下所示:

    config.public_file_server.headers = {
      'Access-Control-Allow-Origin' => 'https://www.example.org'
    }
    

    这适用于我的应用,我不需要更改 Cloudfront 上的任何设置。

    【讨论】:

      【解决方案5】:

      这里是一个 repo,演示了在 Heroku 上使用 Rails 5.2 提供自定义字体。根据https://www.webpagetest.org/,它更进一步并优化了尽可能快地提供字体。

      https://github.com/nzoschke/edgecors

      资产管道和 SCSS

      • 将字体放在app/assets/fonts
      • @font-face 声明放在一个scss 文件中并使用font-url 助手

      来自app/assets/stylesheets/welcome.scss

      @font-face {
        font-family: 'Inconsolata';
        src: font-url('Inconsolata-Regular.ttf') format('truetype');
        font-weight: normal;
        font-style: normal;
      }
      
      body {
        font-family: "Inconsolata";
        font-weight: bold;
      }
      

      使用 CORS 从 CDN 提供服务

      我正在使用 CloudFront,添加了 Heroku Edge addon

      如果您使用自己的 CloudFront,请确保将其配置为将浏览器 Origin 标头转发到您的后端源。

      首先在production.rb中配置一个CDN前缀和默认Cache-Control标头:

      Rails.application.configure do
        # e.g. https://d1unsc88mkka3m.cloudfront.net
        config.action_controller.asset_host = ENV["EDGE_URL"]
      
        config.public_file_server.headers = {
          'Cache-Control' => 'public, max-age=31536000'
        }
      end
      

      如果您尝试从 herokuapp.com URL 访问字体到 CDN URL,您将在浏览器中收到 CORS 错误:

      CORS 策略已阻止从源“https://edgecors.herokuapp.com”访问“https://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf”处的字体: 请求的资源上不存在“Access-Control-Allow-Origin”标头。 edgecors.herokuapp.com/ GEThttps://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttfnet::ERR_FAILED

      所以配置 CORS 以允许从 Heroku 到 CDN URL 访问字体:

      module EdgeCors
        class Application < Rails::Application
          # Initialize configuration defaults for originally generated Rails version.
          config.load_defaults 5.2
      
          config.middleware.insert_after ActionDispatch::Static, Rack::Deflater
      
          config.middleware.insert_before 0, Rack::Cors do
            allow do
              origins %w[
                http://edgecors.herokuapp.com
                https://edgecors.herokuapp.com
              ]
              resource "*", headers: :any, methods: [:get, :post, :options]
            end
          end
        end
      end
      

      提供 gzip 字体资源

      资产管道构建了一个.ttf.gz 文件,但不提供它。此猴子补丁将资产管道 gzip 白名单更改为黑名单:

      require 'action_dispatch/middleware/static'
      
      ActionDispatch::FileHandler.class_eval do
        private
      
          def gzip_file_path(path)
            return false if ['image/png', 'image/jpeg', 'image/gif'].include? content_type(path)
            gzip_path = "#{path}.gz"
            if File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
              gzip_path
            else
              false
            end
          end
      end
      

      最终结果是app/assets/fonts 中的自定义字体文件由长期存在的 CloudFront 缓存提供。

      【讨论】:

        【解决方案6】:

        可能最好使用rack-cors gem。本着自己动手的精神,并跟进@GeekJock 的回答。如果不想使用 rack-cors gem,这是一个可怜的 CORS 标头处理的情况,例如我们只关心静态字体资产(例如替换 font_assets 废弃的 gem)。

        就像在另一个答案中一样,您输入了应用配置:

        config.public_file_server.headers = {
          'Access-Control-Allow-Origin' => '*'
        }
        

        要处理OPTIONS 飞行前请求,您可以在/lib 下的某处编写路由匹配器:

        module FontAssetsConstraint
          FONT_EXTENSIONS = %w[eot svg ttf otf woff woff2].freeze
        
          module_function
        
          def matches?(request)
            extension = request.params["format"]
            extension.present? && FONT_EXTENSIONS.include?(extension)
          end
        end
        

        然后添加路由定义config/routes.rb 来捕捉那些回复:

        Rails.application.routes.draw do
          # Respond to pre-flight CSRF requests for font assets
          if Rails.configuration.public_file_server.enabled &&
             Rails.configuration.public_file_server.headers.include?("Access-Control-Allow-Origin")
            constraints FontAssetsConstraint do
              match "*path", via: :options, to: ->(hash) { [204, Rails.configuration.public_file_server.headers, []] }
            end
          end
        

        或者除了编写路由匹配器和定义之外,您还可以创建自己的中间件来捕获字体:

        class AssetsOptionsResponder
          TYPES = %w(eot svg ttf otf woff woff2).freeze
        
          def initialize(app)
            @app = app
          end
        
          def call(env)
            if env["REQUEST_METHOD"] == "OPTIONS" && targeted?(env["PATH_INFO"])
              [204, access_control_headers, []]
            else
              @app.call(env)
            end
          end
        
          private
        
          def targeted?(pathinfo)
            return if pathinfo.blank?
            TYPES.include? extension(pathinfo)
          end
        
          def extension(pathinfo)
            pathinfo.split("?").first.split(".").last
          end
        
          def access_control_headers
            Rails.configuration.public_file_server.headers
          end
        end
        

        然后在应用配置或初始化程序中,您可以添加此中间件:

        Rails.application.configure do
          if defined?(ActionDispatch::Static) &&
             Rails.configuration.public_file_server.enabled &&
             Rails.configuration.public_file_server.headers.include?("Access-Control-Allow-Origin")
            config.middleware.insert_before ActionDispatch::Static, AssetsOptionsResponder
          end
        end
        

        【讨论】:

          猜你喜欢
          • 2015-05-21
          • 2018-09-23
          • 2013-12-02
          • 1970-01-01
          • 2014-06-29
          • 2020-02-20
          • 2015-08-04
          • 2016-06-04
          • 2013-03-07
          相关资源
          最近更新 更多