在Rails应用程序

时间:2015-09-15 17:52:04

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

访问我的网站时,我一直收到来自控制台的错误消息:

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
    

但没有,零,纳达。

我在Heroku上使用Rails 4.1。

5 个答案:

答案 0 :(得分:65)

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

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

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

  2. 我发现那么多过时的信息 - 博客帖子和SO帖子无穷无尽。自从许多帖子以来,CloudFront已经显着改进了其CORS支持,尽管它仍然不完美。 (CORS应该是开箱即用的。)宝石本身也在不断发展。

  3. 我的设置:在Heroku上运行的Rails 4.1.15,其资产来自CloudFront。我的应用程序响应http和https,在" www。"和区域顶点,没有进行任何重定向。

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

    1. <强>的Gemfile

      gem "rack-cors"
      

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

    2. ,这一点至关重要
    3. <强>配置/初始化/机架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应用程序(以及代表我们的Rails应用程序(以及扩展,在CloudFront上,因为它正在镜像我们)访问资源) malicious-site.com)并且仅适用于/assets/网址(而不是适用于我们的控制器)。换句话说,允许CloudFront为资产提供服务,但不要再开门了。

      注意:

      • 我尝试在 rack-timeout之后插入此,而不是在中间件链的头部。 尽管如此,它仍然适用于开发,但并没有在Heroku上进行 拥有相同的中间件(Honeybadger除外)。
      • 原点列表也可以作为Regexps完成。 小心将模式锚定在字符串末尾。

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

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

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

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

      请求标题白名单有点埋没。

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


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

      • https://console.aws.amazon.com/cloudfront/home#distributions
      • 单击“创建分发”

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

      • 投放方式:网络(非RTMP)

      • 原点设置

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

        • 查看器协议策略:将HTTP重定向到HTTPS
        • 转发标题:白名单
        • 白名单标题:选择来源,然后点击添加&gt;&gt;
        • 自动压缩对象:是
    5. 更改所有这些内容后,请记住,任何旧的缓存值都可能需要一些时间才能从CloudFront过期。您可以通过转到CloudFront分配的“无效”标签并为*创建无效来明确使缓存的资产无效。

答案 1 :(得分:8)

如果您在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配置文件,这是不理想的,因为每次乘客升级和模板更改时都需要维护此副本。

===

以下是摘要:

导航到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

然后部署......

===

如果您知道更好的解决方案,请填写评论。

实施后,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

答案 2 :(得分:2)

我遇到了同样的问题,并设法解决了这个问题。

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

要解决此问题,您需要在Heroku上添加正确的CORS标头。幸运的是,这很容易。

首先,将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

答案 3 :(得分:0)

从5.0版开始,Rails允许为资产设置自定义HTTP标头,而您不必使用rack-cors或font-assets宝石。为了为资产(包括字体)设置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上更改任何设置。

答案 4 :(得分:0)

这里是一个回购,演示了如何在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;
}

通过CDN与CORS一起提供服务

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

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

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

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://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf”访问“ https://edgecors.herokuapp.com”的字体:   所请求的资源上没有“ Access-Control-Allow-Origin”标头。 edgecors.herokuapp.com/   GET https://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf net :: 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缓存提供的。