簡體   English   中英

Cloudfront CORS 問題在 Rails 應用程序上提供字體

[英]Cloudfront CORS issue serving fonts on Rails application

訪問我的網站時,我不斷從控制台收到此錯誤消息:

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 上的白名單標頭,如本文所述

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

但沒有,零,納達。

我在 Heroku 上使用 Rails 4.1。

這是一個非常難以處理的問題,原因有兩個:

  1. CloudFront正在鏡像我們的Rails應用程序的響應標頭這一事實需要您扭轉局面。 CORS協議很難理解,但現在你必須在兩個層面上遵循它:瀏覽器和CloudFront之間(當我們的Rails應用程序將它用作CDN時)​​,以及瀏覽器和我們的Rails應用程序之間(當一些惡意網站想濫用我們)。

    CORS實際上是關於瀏覽器和網頁想要訪問的第三方資源之間的對話。 (在我們的使用案例中,這是CloudFront CDN,為我們的應用程序提供資產。)但是,由於CloudFront 我們的應用程序獲取其Access-Control響應標頭我們的應用程序需要提供這些標頭,就好像它是CloudFront通話, 同時不是授予權限,使其自身暴露於導致首先開發同源策略/ CORS的濫用類型。 特別是,我們不應授予*訪問我們網站上*資源的權限。

  2. 我發現了那么多過時的信息 - 無窮無盡的博客文章和SO線程。 自從許多帖子以來,CloudFront已經顯着改進了其CORS支持,盡管它仍然不完美。 (CORS應該是開箱即用的。)寶石本身也在不斷發展。

我的設置:Rails 4.1.15在Heroku上運行,資產由CloudFront提供。 我的應用程序在“www”上響應http和https。 和區域頂點,沒有進行任何重定向。

我簡要地看了一下問題中提到的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,以及通過“www。”和裸域)訪問時,這是至關重要的

  2. 配置/初始化/機架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應用程序(而不是代表malicious-site.com)訪問我們的Rails應用程序(以及擴展,在CloudFront上,因為它正在鏡像我們)上的資源,並且僅用於/assets/ urls(而不是我們的控制器)。 換句話說,允許CloudFront為資產提供服務,但不要再開門了。

    筆記:

    • 我嘗試機架超時之后插入它而不是在中間件鏈的頭部。 盡管擁有相同的中間件(除了Honeybadger),它仍然可以在dev上工作,但並未參與Heroku。
    • 起源列表也可以作為Regexps完成。 小心將模式錨定在字符串末尾。

       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
      • 轉發標題:白名單
      • 白名單標題:選擇Origin並單擊添加>>
      • 自動壓縮對象:是的

更改所有這些內容后,請記住,任何舊的緩存值都可能需要一些時間才能從CloudFront到期。 您可以通過轉到CloudFront分配的“失效”選項卡並為*創建失效來明確使緩存的資產無效。

如果您在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

我剛才遇到了同樣的問題,並設法解決了這個問題。

您已正確告知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

從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上的任何設置。

這是一個repo,使用適用於Heroku的Rails 5.2提供自定義字體。 它進一步優化了根據https://www.webpagetest.org/盡可能快地提供字體服務

https://github.com/nzoschke/edgecors

資產管道和SCSS

  • 將字體放在app/assets/fonts
  • @font-face聲明放在scss文件中並使用font-url helper

來自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插件

如果您使用自己的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錯誤:

來自' https://edgecors.herokuapp.com '的' https://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf '訪問字體已被CORS政策阻止:沒有'訪問控制 - 允許-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

最終結果是從長期存在的CloudFront緩存提供的app/assets/fonts的自定義字體文件。

可能最好的是使用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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM