web-dev-qa-db-fra.com

Problème CORS Cloudfront servant des polices sur l'application Rails

Je continue à recevoir ce message d'erreur de la console lorsque je visite mon site Web:

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.

J'ai tout essayé:

  • J'ai installé le gem font_assets
  • configuré le fichier application.rb

    config.font_assets.Origin = 'http://example.com'
    
  • En-têtes de liste blanche sur Cloudfront, comme expliqué dans cet article à

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

Mais rien, zéro, nada.

J'utilise Rails 4.1 sur Heroku.

21
Manuel F.

C'était une question incroyablement difficile à traiter, pour deux raisons:

  1. Le fait que CloudFront soit en miroir les en-têtes de réponse de notre application Rails vous oblige à changer vos idées. Le protocole CORS est assez difficile à comprendre, mais vous devez maintenant le suivre à deux niveaux: entre le navigateur et CloudFront (lorsque notre application Rails l’utilise en tant que CDN), et entre le navigateur et notre application Rails (lorsque un site malveillant veut abuser de nous).

    CORS consiste en réalité en un dialogue entre le navigateur et les ressources tierces auxquelles une page Web souhaite accéder. (Dans notre cas d'utilisation, il s'agit du CDN CloudFront, qui sert des actifs à notre application.) Mais depuis que CloudFront obtient ses en-têtes de réponse Access-Control de notre application, notre application doit les servir comme if c'est CloudFront qui parle, et simultanément n'accorde pas d'autorisations qui s'exposeraient au type d'abus ayant conduit à la création de la stratégie de même origine/CORS. En particulier, nous ne devrions pas accorder un accès * aux ressources * de notre site.

  2. J'ai trouvé so much des informations obsolètes - une ligne infinie de billets de blog et de fils SO. CloudFront a considérablement amélioré la prise en charge de CORS depuis plusieurs de ces publications, même si elle n’est toujours pas parfaite. (La SCRO devrait vraiment être traitée immédiatement). Et les pierres précieuses elles-mêmes ont évolué.

Ma configuration: Rails 4.1.15 s'exécutant sur Heroku, avec des actifs servis depuis CloudFront. Mon application répond à la fois à http et à https, à la fois sur "www." et le sommet de la zone, sans aucune redirection.

J'ai brièvement regardé la pierre précieuse de font_assets mentionnée dans la question, mais je l'ai rapidement abandonnée au profit de rack-cors, ce qui semblait plus pertinent. Je ne voulais pas simplement ouvrir toutes les origines et tous les chemins, car cela irait à l'encontre de l'objectif de la SCRO et de la sécurité de la politique de la même origine. Je devais donc pouvoir spécifier les quelques origines que je permettrais. Enfin, je préfère personnellement configurer Rails via des fichiers config/initializers/*.rb individuels plutôt que de modifier les fichiers de configuration standard (tels que config.ru ou config/application.rb). En résumé, voici ma solution, qui, à mon avis, est la meilleure disponible, à compter du 2016-04-16:

  1. Gemfile

    gem "rack-cors"
    

    La gem rack-cors implémente le protocole CORS dans un middleware Rack. En plus de définir des en-têtes Access-Control-Allow-Allow et les environnements associés sur les origines approuvées, il ajoute un en-tête de réponse Vary: Origin, en ordonnant à CloudFront de mettre en cache les réponses. (y compris les en-têtes de réponse) pour chaque origine séparément. Ceci est crucial lorsque notre site est accessible via plusieurs origines (par exemple via http et https, et via "www." Et le domaine nu).

  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
    

    Cela indique au navigateur qu'il peut accéder aux ressources de notre application Rails (et par extension, à CloudFront, car il nous reflète) uniquement pour le compte de notre application Rails (et pas au nom du site malveillant. com) et uniquement pour les URL /assets/ (et non pour nos contrôleurs). En d'autres termes, autorisez CloudFront à gérer des actifs mais n'ouvrez pas la porte plus que nécessaire.

    Remarques:

    • J'ai essayé d'insérer ce après rack-timeout au lieu de le placer en tête de la chaîne de middleware. Cela fonctionnait sur dev mais ne donnait pas le coup d'envoi à Heroku, même si Avait le même middleware (autre que Honeybadger).
    • La liste des origines peut aussi être faite sous forme de regexps. Veillez à ancrer les motifs en fin de chaîne.

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

      mais je pense que c’est plus facile de lire des chaînes littérales.

  3. Configurez CloudFront pour transmettre l'en-tête de requête Origin du navigateur à notre application Rails.

    Bizarrement, il semble que CloudFront transmette l'en-tête Origin du navigateur à notre application Rails indépendamment de si nous l'ajoutons ici, mais que CloudFront respecte la directive de mise en cache Vary: Origin de notre application uniquement si Origin est explicitement ajouté à la liste blanche des en-têtes ( avril 2016).

    La liste blanche d'en-tête de demande est un peu enterrée.

    Si la distribution existe déjà, vous pouvez la trouver à:

    • https://console.aws.Amazon.com/cloudfront/home#distributions
    • sélectionner la distribution
    • cliquez sur Paramètres de distribution
    • allez dans l'onglet Comportements
    • sélectionnez le comportement (il n'y en aura probablement qu'un)
    • Cliquez sur Modifier
    • En-têtes avant: liste blanche
    • En-têtes de la liste blanche: Sélectionnez Origine et cliquez sur Ajouter >>


    Si vous n'avez pas encore créé la distribution, créez-la à l'adresse:

    • https://console.aws.Amazon.com/cloudfront/home#distributions
    • Cliquez sur Créer une distribution

      (Par souci d'exhaustivité et de reproductibilité, je liste tous les paramètres que j'ai modifiés par défaut, mais les paramètres de la liste blanche sont les seuls qui sont pertinents pour cette discussion.)

    • Méthode de livraison: Web (pas RTMP)

    • Paramètres d'origine

      • Nom de domaine d'origine: exemple.com
      • Protocoles SSL d'origine: TLSv1.2 UNIQUEMENT
      • Stratégie de protocole d'origine: HTTPS uniquement
    • Paramètres de comportement du cache par défaut

      • Politique de protocole du visualiseur: redirection HTTP vers HTTPS
      • En-têtes avant: liste blanche
      • En-têtes de la liste blanche: Sélectionnez Origine et cliquez sur Ajouter >>
      • Compresser les objets automatiquement: Oui

Après avoir modifié toutes ces choses, rappelez-vous qu'il peut falloir un certain temps pour que toutes les anciennes valeurs mises en cache expirent à partir de CloudFront. Vous pouvez explicitement invalider des actifs mis en cache en accédant à l'onglet Invalidations de la distribution CloudFront et en créant une invalidation pour *.

61
Noach Magedman

Si vous utilisez Rails on Passenger and Heroku: (sinon, passez directement à la réponse de Noach Magedman)

La réponse de Noach Magedman m'a été très utile pour configurer correctement CloudFront.

J'ai également installé rack-cors exactement comme décrit et, même si cela fonctionnait bien en développement, les commandes CURL en production ne renvoyaient aucune des configurations 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

Notez que je coche le serveur directement sans passer par le CDN, qui, après avoir invalidé tout le contenu, doit simplement transférer la réponse du serveur. La ligne importante ici est Server: nginx/1.10.0, ce qui indique que les actifs sont servis par nginx et non par Rails. En conséquence, les configurations rack-cors ne s'appliquent pas.

La solution qui a fonctionné pour nous est la suivante: http://monksealsoftware.com/Ruby-on-Rails-cors-heroku-passenger-5-0-28/

Il s’agissait essentiellement de cloner et de modifier le fichier de configuration nginx pour Passenger, ce qui n’est pas idéal, car cette copie doit être conservée à chaque fois que Passenger est mis à niveau et que le modèle est modifié.

===

Voici un résumé:

Naviguez jusqu'au dossier racine de votre projet Rails et copiez le modèle de configuration nginx.

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

Ouvrez config/passenger_config.erb et commentez cette ligne

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

Ajoutez ces configurations sous la ligne mentionnée ci-dessus

### 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 ###

Changez la Procfile pour inclure ce fichier de configuration personnalisé.

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

Alors déployez ...

===

Si vous connaissez une meilleure solution, veuillez inclure les commentaires.

Après la mise en œuvre, la commande CURL a généré la réponse suivante:

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
8
migu

Je viens d'avoir le même problème et j'ai réussi à le résoudre.

Vous avez correctement demandé à Cloudfront d'autoriser ces en-têtes, mais vous n'avez pas ajouté ces en-têtes à l'endroit où Cloudfront obtient la police. Oui, vos en-têtes Origin sont autorisés, mais Heroku n'envoie pas ces en-têtes avec la police.

Pour résoudre ce problème, vous devez obtenir les en-têtes CORS appropriés ajoutés à la police sur Heroku. Heureusement, c'est assez facile.

Tout d’abord, ajoutez la gemme rack/cors à votre projet. https://github.com/cyu/rack-cors

Ensuite, configurez votre serveur Rack pour charger et configurer CORS pour tous les actifs qu’il sert. Ajoutez les éléments suivants après le préchargement de votre application dans 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

Ainsi, toutes les ressources renvoyées par Heroku seront appliquées aux en-têtes CORS appropriés. Vous pouvez restreindre l’application des en-têtes en fonction de vos fichiers et de vos besoins en matière de sécurité.

Une fois déployé, accédez à Cloudfront et commencez une invalidation sur tout ce qui vous avait précédemment fourni une erreur d'autorisation CORS. Désormais, lorsque Cloudfront charge une nouvelle copie à partir de Heroku, il possède les en-têtes appropriés. Cloudfront les transmettra au client comme précédemment configuré avec vos autorisations Origin.

Pour vous assurer de fournir les en-têtes appropriés à partir de votre serveur, vous pouvez utiliser la commande curl suivante pour valider vos en-têtes: curl -I -s -X GET -H "Origin: www.yoursite.com" http://www.yoursite.dev:5000/assets/fonts/myfont.svg

Vous devriez voir les en-têtes suivants renvoyés:

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
Joe Burns

Voici un repo qui montre une police personnalisée avec Rails 5.2 qui fonctionne sur Heroku. Il va plus loin et optimise l'utilisation des polices pour qu'elles soient aussi rapides que possible selon https://www.webpagetest.org/

https://github.com/nzoschke/edgecors

Asset Pipeline et SCSS

  • Placez les polices dans app/assets/fonts
  • Placez la déclaration @font-face dans un fichier scss et utilisez l'assistant font-url

De 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;
}

Servir à partir de CDN avec CORS

J'utilise CloudFront, ajouté avec l'addon Heroku Edge .

Si vous utilisez votre propre CloudFront, assurez-vous de le configurer pour transférer l'en-tête Origin du navigateur vers votre origine dorsale.

Configurez d’abord un préfixe CDN et des en-têtes Cache-Control par défaut dans 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

Si vous essayez d'accéder à la police de l'URL herokuapp.com à l'URL du CDN, vous obtiendrez une erreur CORS dans votre navigateur:

Accès à la police sur ' https://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf ' from Origin ' https://edgecors.herokuapp.com ' a été bloqué par la politique CORS : Aucun en-tête 'Access-Control-Allow-Origin' n'est présent sur la ressource demandée. edgecors.herokuapp.com/ GET https://d1unsc88mkka3m.cloudfront.net/assets/Inconsolata-Regular.ttf net :: ERR_FAILED

Configurez donc CORS pour autoriser l’accès à la police de Heroku vers l’URL du CDN: 

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

Servir gzip Font Asset

Le pipeline d'actifs crée un fichier .ttf.gz mais ne le sert pas. Ce patch de singe modifie la liste blanche gzip du pipeline d’actifs en liste noire:

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

Le résultat final est un fichier de police personnalisé dans app/assets/fonts servi à partir d'un cache CloudFront de longue durée.

0
Noah Zoschke

À partir de la version 5.0, Rails permet de définir des en-têtes HTTP personnalisés pour les actifs. Vous n'avez pas à utiliser les gems rack-cors ou font-assets. Afin de définir Access-Control-Allow-Origin pour les actifs (y compris les polices), ajoutez simplement le code suivant à config/environment/production.rb:

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

La valeur de l'en-tête pourrait également être un domaine spécifique, tel que le suivant:

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

Cela a fonctionné pour mon application et je n'ai pas eu besoin de modifier les paramètres sur Cloudfront.

0
GeekJock