Activer les demandes d’origine croisée dans API Web ASP.NET 2

Par Mike Wasson

Ce contenu concerne une version précédente de .NET. Le nouveau développement doit utiliser ASP.NET Core. Pour plus d’informations sur l’utilisation de l’API web et des demandes CORS (Cross-Origin Requests) dans ASP.NET Core, consultez :

La sécurité des navigateurs empêche une page web d’adresser des demandes AJAX à un autre domaine. Cette restriction est appelée stratégie de même origine et empêche un site malveillant de lire des données sensibles à partir d’un autre site. Toutefois, vous souhaitez parfois laisser d’autres sites appeler votre API web.

Le partage de ressources entre les origines (CORS) est une norme W3C qui permet à un serveur d’assouplir la stratégie de même origine. Grâce au mécanisme CORS, un serveur peut autoriser explicitement certaines demandes multi-origines tout en en refusant d’autres. CORS est plus sûr et plus flexible que les techniques antérieures telles que JSONP. Ce tutoriel montre comment activer CORS dans votre application API web.

Logiciels utilisés dans le tutoriel

Introduction

Ce tutoriel montre la prise en charge de CORS dans API Web ASP.NET. Nous allons commencer par créer deux projets ASP.NET : l’un appelé « WebService », qui héberge un contrôleur d’API web, et l’autre appelé « WebClient », qui appelle WebService. Étant donné que les deux applications sont hébergées dans des domaines différents, une requête AJAX de WebClient vers WebService est une requête d’origine croisée.

Affiche le service web et le client web

Qu’est-ce que la « même origine » ?

Deux URL ont la même origine si elles ont des schémas, des hôtes et des ports identiques. (RFC 6454)

Ces deux URL ont la même origine :

  • http://example.com/foo.html
  • http://example.com/bar.html

Ces URL ont des origines différentes des deux précédentes :

  • http://example.net : Domaine différent
  • http://example.com:9000/foo.html : Port différent
  • https://example.com/foo.html : Schéma différent
  • http://www.example.com/foo.html : Sous-domaine différent

Notes

Internet Explorer ne prend pas en compte le port lors de la comparaison des origines.

Créer le projet WebService

Notes

Cette section suppose que vous savez déjà comment créer des projets d’API web. Si ce n’est pas le cas, consultez Prise en main avec API Web ASP.NET.

  1. Démarrez Visual Studio et créez un projet d’application web ASP.NET (.NET Framework).

  2. Dans la boîte de dialogue Nouvelle application web ASP.NET , sélectionnez le modèle de projet Vide . Sous Ajouter des dossiers et des références principales pour, cochez la case API web .

    Boîte de dialogue Nouveau projet ASP.NET dans Visual Studio

  3. Ajoutez un contrôleur d’API web nommé TestController avec le code suivant :

    using System.Net.Http;
    using System.Web.Http;
    
    namespace WebService.Controllers
    {
        public class TestController : ApiController
        {
            public HttpResponseMessage Get()
            {
                return new HttpResponseMessage()
                {
                    Content = new StringContent("GET: Test message")
                };
            }
    
            public HttpResponseMessage Post()
            {
                return new HttpResponseMessage()
                {
                    Content = new StringContent("POST: Test message")
                };
            }
    
            public HttpResponseMessage Put()
            {
                return new HttpResponseMessage()
                {
                    Content = new StringContent("PUT: Test message")
                };
            }
        }
    }
    
  4. Vous pouvez exécuter l’application localement ou déployer sur Azure. (Pour les captures d’écran de ce didacticiel, l’application se déploie sur Azure App Service Web Apps.) Pour vérifier que l’API web fonctionne, accédez à http://hostname/api/test/, où nom d’hôte est le domaine dans lequel vous avez déployé l’application. Vous devez voir le texte de réponse « GET : Message de test ».

    Navigateur web affichant le message de test

Créer le projet WebClient

  1. Créez un autre projet d’application web ASP.NET (.NET Framework) et sélectionnez le modèle de projet MVC . Si vous le souhaitez, sélectionnez Modifier l’authentification>Sans authentification. Vous n’avez pas besoin d’authentification pour ce tutoriel.

    Modèle MVC dans la boîte de dialogue Nouveau projet ASP.NET dans Visual Studio

  2. Dans Explorateur de solutions, ouvrez le fichier Views/Home/Index.cshtml. Remplacez le code dans ce fichier par les éléments suivants :

    <div>
        <select id="method">
            <option value="get">GET</option>
            <option value="post">POST</option>
            <option value="put">PUT</option>
        </select>
        <input type="button" value="Try it" onclick="sendRequest()" />
        <span id='value1'>(Result)</span>
    </div>
    
    @section scripts {
    <script>
        // TODO: Replace with the URL of your WebService app
        var serviceUrl = 'http://mywebservice/api/test'; 
    
        function sendRequest() {
            var method = $('#method').val();
    
            $.ajax({
                type: method,
                url: serviceUrl
            }).done(function (data) {
                $('#value1').text(data);
            }).fail(function (jqXHR, textStatus, errorThrown) {
                $('#value1').text(jqXHR.responseText || textStatus);
            });
        }
    </script>
    }
    

    Pour la variable serviceUrl , utilisez l’URI de l’application WebService.

  3. Exécutez l’application WebClient localement ou publiez-la sur un autre site web.

Lorsque vous cliquez sur le bouton « Essayer », une demande AJAX est envoyée à l’application WebService à l’aide de la méthode HTTP répertoriée dans la zone de liste déroulante (GET, POST ou PUT). Cela vous permet d’examiner différentes demandes d’origine croisée. Actuellement, l’application WebService ne prend pas en charge CORS. Par conséquent, si vous cliquez sur le bouton, vous obtiendrez une erreur.

Erreur « Essayer » dans le navigateur

Notes

Si vous watch le trafic HTTP dans un outil comme Fiddler, vous verrez que le navigateur envoie la requête GET et que la requête réussit, mais que l’appel AJAX retourne une erreur. Il est important de comprendre que la stratégie de même origine n’empêche pas le navigateur d’envoyer la demande. Au lieu de cela, cela empêche l’application de voir la réponse.

Débogueur web Fiddler affichant des requêtes web

Activez CORS

Maintenant, nous allons activer CORS dans l’application WebService. Tout d’abord, ajoutez le package NuGet CORS. Dans Visual Studio, dans le menu Outils , sélectionnez Gestionnaire de package NuGet, puis console du gestionnaire de package. Dans la fenêtre Console du Gestionnaire de package, tapez la commande suivante :

Install-Package Microsoft.AspNet.WebApi.Cors

Cette commande installe le dernier package et met à jour toutes les dépendances, y compris les bibliothèques d’API web principales. Utilisez l’indicateur -Version pour cibler une version spécifique. Le package CORS nécessite l’API Web 2.0 ou ultérieure.

Ouvrez le fichier App_Start/WebApiConfig.cs. Ajoutez le code suivant à la méthode WebApiConfig.Register :

using System.Web.Http;
namespace WebService
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // New code
            config.EnableCors();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

Ensuite, ajoutez l’attribut [EnableCors] à la TestController classe :

using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Cors;

namespace WebService.Controllers
{
    [EnableCors(origins: "http://mywebclient.azurewebsites.net", headers: "*", methods: "*")]
    public class TestController : ApiController
    {
        // Controller methods not shown...
    }
}

Pour le paramètre origines , utilisez l’URI où vous avez déployé l’application WebClient. Cela autorise les requêtes d’origine croisée de WebClient, tout en refusant toutes les autres requêtes inter-domaines. Plus tard, je vais décrire les paramètres de [EnableCors] plus en détail.

N’incluez pas de barre oblique à la fin de l’URL d’origine .

Redéployez l’application WebService mise à jour. Vous n’avez pas besoin de mettre à jour WebClient. À présent, la requête AJAX de WebClient doit réussir. Les méthodes GET, PUT et POST sont toutes autorisées.

Navigateur web affichant le message de test réussi

Fonctionnement de CORS

Cette section décrit ce qui se passe dans une requête CORS, au niveau des messages HTTP. Il est important de comprendre le fonctionnement de CORS afin de pouvoir configurer correctement l’attribut [EnableCors] et résoudre les problèmes si les choses ne fonctionnent pas comme prévu.

La spécification CORS introduit plusieurs nouveaux en-têtes HTTP qui activent les demandes d’origine croisée. Si un navigateur prend en charge CORS, il définit automatiquement ces en-têtes pour les demandes d’origine croisée ; vous n’avez pas besoin de faire quelque chose de spécial dans votre code JavaScript.

Voici un exemple de demande d’origine croisée. L’en-tête « Origine » indique le domaine du site qui effectue la demande.

GET http://myservice.azurewebsites.net/api/test HTTP/1.1
Referer: http://myclient.azurewebsites.net/
Accept: */*
Accept-Language: en-US
Origin: http://myclient.azurewebsites.net
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: myservice.azurewebsites.net

Si le serveur autorise la requête, il définit l’en-tête Access-Control-Allow-Origin. La valeur de cet en-tête correspond à l’en-tête Origin ou est la valeur générique « * », ce qui signifie que toute origine est autorisée.

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: text/plain; charset=utf-8
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Date: Wed, 05 Jun 2013 06:27:30 GMT
Content-Length: 17

GET: Test message

Si la réponse n’inclut pas l’en-tête Access-Control-Allow-Origin, la requête AJAX échoue. Plus précisément, le navigateur interdit la demande. Même si le serveur retourne une réponse réussie, le navigateur ne rend pas la réponse disponible pour l’application cliente.

Demandes en pré-vol

Pour certaines requêtes CORS, le navigateur envoie une requête supplémentaire, appelée « demande de contrôle préliminaire », avant d’envoyer la demande réelle pour la ressource.

Le navigateur peut ignorer la demande de contrôle préliminaire si les conditions suivantes sont remplies :

  • La méthode de requête est GET, HEAD ou POST, et

  • L’application ne définit pas d’en-têtes de requête autres que Accept, Accept-Language, Content-Language, Content-Type ou Last-Event-ID, et

  • L’en-tête Content-Type (si défini) est l’un des éléments suivants :

    • application/x-www-form-urlencoded
    • multipart/form-data
    • texte/brut

La règle relative aux en-têtes de requête s’applique aux en-têtes que l’application définit en appelant setRequestHeader sur l’objet XMLHttpRequest . (La spécification CORS appelle ces « en-têtes de requête d’auteur ».) La règle ne s’applique pas aux en-têtes que le navigateur peut définir, tels que User-Agent, Host ou Content-Length.

Voici un exemple de demande de pré-vol :

OPTIONS http://myservice.azurewebsites.net/api/test HTTP/1.1
Accept: */*
Origin: http://myclient.azurewebsites.net
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: accept, x-my-custom-header
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)
Host: myservice.azurewebsites.net
Content-Length: 0

La demande de préversion utilise la méthode HTTP OPTIONS. Il comprend deux en-têtes spéciaux :

  • Access-Control-Request-Method : méthode HTTP qui sera utilisée pour la requête réelle.
  • Access-Control-Request-Headers : liste des en-têtes de requête définis par l’application sur la demande réelle. (Là encore, cela n’inclut pas les en-têtes que le navigateur définit.)

Voici un exemple de réponse, en supposant que le serveur autorise la demande :

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 0
Access-Control-Allow-Origin: http://myclient.azurewebsites.net
Access-Control-Allow-Headers: x-my-custom-header
Access-Control-Allow-Methods: PUT
Date: Wed, 05 Jun 2013 06:33:22 GMT

La réponse inclut un en-tête Access-Control-Allow-Methods qui répertorie les méthodes autorisées et éventuellement un en-tête Access-Control-Allow-Headers, qui répertorie les en-têtes autorisés. Si la demande de pré-vol réussit, le navigateur envoie la demande réelle, comme décrit précédemment.

Les outils couramment utilisés pour tester des points de terminaison avec des demandes d’OPTIONS en amont (par exemple, Fiddler et Postman) n’envoient pas les en-têtes OPTIONS requis par défaut. Vérifiez que les Access-Control-Request-Method en-têtes et Access-Control-Request-Headers sont envoyés avec la demande et que les en-têtes OPTIONS atteignent l’application via IIS.

Pour configurer IIS afin de permettre à une application ASP.NET de recevoir et de gérer les requêtes OPTION, ajoutez la configuration suivante au fichier web.config de l’application dans la <system.webServer><handlers> section :

<system.webServer>
  <handlers>
    <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
    <remove name="OPTIONSVerbHandler" />
    <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
  </handlers>
</system.webServer>

La suppression de empêche OPTIONSVerbHandler IIS de gérer les demandes OPTIONS. Le remplacement de permet aux demandes OPTIONS d’atteindre ExtensionlessUrlHandler-Integrated-4.0 l’application, car l’inscription de module par défaut autorise uniquement les requêtes GET, HEAD, POST et DEBUG avec des URL sans extension.

Règles d’étendue pour [EnableCors]

Vous pouvez activer CORS par action, par contrôleur ou globalement pour tous les contrôleurs d’API web de votre application.

Par action

Pour activer CORS pour une action unique, définissez l’attribut [EnableCors] sur la méthode d’action. L’exemple suivant active CORS pour la GetItem méthode uniquement.

public class ItemsController : ApiController
{
    public HttpResponseMessage GetAll() { ... }

    [EnableCors(origins: "http://www.example.com", headers: "*", methods: "*")]
    public HttpResponseMessage GetItem(int id) { ... }

    public HttpResponseMessage Post() { ... }
    public HttpResponseMessage PutItem(int id) { ... }
}

Par contrôleur

Si vous définissez [EnableCors] sur la classe du contrôleur, il s’applique à toutes les actions sur le contrôleur. Pour désactiver CORS pour une action, ajoutez l’attribut [DisableCors] à l’action. L’exemple suivant active CORS pour chaque méthode à l’exception PutItemde .

[EnableCors(origins: "http://www.example.com", headers: "*", methods: "*")]
public class ItemsController : ApiController
{
    public HttpResponseMessage GetAll() { ... }
    public HttpResponseMessage GetItem(int id) { ... }
    public HttpResponseMessage Post() { ... }

    [DisableCors]
    public HttpResponseMessage PutItem(int id) { ... }
}

Globalement

Pour activer CORS pour tous les contrôleurs d’API web de votre application, passez un instance EnableCorsAttribute à la méthode EnableCors :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var cors = new EnableCorsAttribute("www.example.com", "*", "*");
        config.EnableCors(cors);
        // ...
    }
}

Si vous définissez l’attribut sur plusieurs étendues, l’ordre de priorité est :

  1. Action
  2. Contrôleur
  3. Global

Définir les origines autorisées

Le paramètre origins de l’attribut [EnableCors] spécifie les origines autorisées à accéder à la ressource. La valeur est une liste séparée par des virgules des origines autorisées.

[EnableCors(origins: "http://www.contoso.com,http://www.example.com", 
    headers: "*", methods: "*")]

Vous pouvez également utiliser la valeur générique « * » pour autoriser les demandes provenant de n’importe quelle origine.

Réfléchissez attentivement avant d’autoriser les demandes provenant de n’importe quelle origine. Cela signifie que littéralement n’importe quel site web peut effectuer des appels AJAX à votre API web.

// Allow CORS for all origins. (Caution!)
[EnableCors(origins: "*", headers: "*", methods: "*")]

Définir les méthodes HTTP autorisées

Le paramètre methods de l’attribut [EnableCors] spécifie les méthodes HTTP autorisées à accéder à la ressource. Pour autoriser toutes les méthodes, utilisez la valeur générique « * ». L’exemple suivant autorise uniquement les requêtes GET et POST.

[EnableCors(origins: "http://www.example.com", headers: "*", methods: "get,post")]
public class TestController : ApiController
{
    public HttpResponseMessage Get() { ... }
    public HttpResponseMessage Post() { ... }
    public HttpResponseMessage Put() { ... }    
}

Définir les en-têtes de requête autorisés

Cet article décrit plus haut comment une demande de pré-survol peut inclure un en-tête Access-Control-Request-Headers, répertoriant les en-têtes HTTP définis par l’application (les « en-têtes de requête d’auteur »). Le paramètre en-têtes de l’attribut [EnableCors] spécifie les en-têtes de requête d’auteur qui sont autorisés. Pour autoriser les en-têtes, définissez les en-têtes sur « * ». Pour autoriser des en-têtes spécifiques, définissez les en-têtes sur une liste séparée par des virgules des en-têtes autorisés :

[EnableCors(origins: "http://example.com", 
    headers: "accept,content-type,origin,x-my-header", methods: "*")]

Toutefois, les navigateurs ne sont pas entièrement cohérents dans la façon dont ils définissent Access-Control-Request-Headers. Par exemple, Chrome inclut actuellement « origin ». FireFox n’inclut pas d’en-têtes standard tels que « Accept », même lorsque l’application les définit en script.

Si vous définissez des en-têtes sur autre chose que « * », vous devez inclure au moins « accept », « content-type » et « origin », ainsi que tous les en-têtes personnalisés que vous souhaitez prendre en charge.

Définir les en-têtes de réponse autorisés

Par défaut, le navigateur n’expose pas tous les en-têtes de réponse à l’application. Les en-têtes de réponse disponibles par défaut sont les suivants :

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

La spécification CORS appelle ces en-têtes de réponse simples. Pour rendre d’autres en-têtes disponibles pour l’application, définissez le paramètre exposedHeaders de [EnableCors].

Dans l’exemple suivant, la méthode du Get contrôleur définit un en-tête personnalisé nommé « X-Custom-Header ». Par défaut, le navigateur n’expose pas cet en-tête dans une demande d’origine croisée. Pour rendre l’en-tête disponible, incluez « X-Custom-Header » dans exposedHeaders.

[EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "X-Custom-Header")]
public class TestController : ApiController
{
    public HttpResponseMessage Get()
    {
        var resp = new HttpResponseMessage()
        {
            Content = new StringContent("GET: Test message")
        };
        resp.Headers.Add("X-Custom-Header", "hello");
        return resp;
    }
}

Passer des informations d’identification dans les demandes inter-origines

Les informations d’identification nécessitent une gestion spéciale dans une requête CORS. Par défaut, le navigateur n’envoie pas d’informations d’identification avec une demande d’origine croisée. Les informations d’identification incluent les cookies ainsi que les schémas d’authentification HTTP. Pour envoyer des informations d’identification avec une demande d’origine croisée, le client doit définir XMLHttpRequest.withCredentials sur true.

Utilisation directe de XMLHttpRequest :

var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;

Dans jQuery :

$.ajax({
    type: 'get',
    url: 'http://www.example.com/api/test',
    xhrFields: {
        withCredentials: true
    }

En outre, le serveur doit autoriser les informations d’identification. Pour autoriser les informations d’identification d’origine croisée dans l’API web, définissez la propriété SupportsCredentials sur true sur l’attribut [EnableCors] :

[EnableCors(origins: "http://myclient.azurewebsites.net", headers: "*", 
    methods: "*", SupportsCredentials = true)]

Si cette propriété a la valeur true, la réponse HTTP inclut un en-tête Access-Control-Allow-Credentials. Cet en-tête indique au navigateur que le serveur autorise les informations d’identification pour une demande d’origine croisée.

Si le navigateur envoie des informations d’identification, mais que la réponse n’inclut pas d’en-tête Access-Control-Allow-Credentials valide, le navigateur n’expose pas la réponse à l’application et la demande AJAX échoue.

Veillez à définir SupportsCredentials sur true, car cela signifie qu’un site web d’un autre domaine peut envoyer les informations d’identification d’un utilisateur connecté à votre API web pour le compte de l’utilisateur, sans que l’utilisateur en soit informé. La spécification CORS indique également que la définition des origines sur « * » n’est pas valide si SupportsCredentials a la valeur true.

Fournisseurs de stratégie CORS personnalisés

L’attribut [EnableCors] implémente l’interface ICorsPolicyProvider. Vous pouvez fournir votre propre implémentation en créant une classe qui dérive de Attribute et implémente ICorsPolicyProvider.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class MyCorsPolicyAttribute : Attribute, ICorsPolicyProvider 
{
    private CorsPolicy _policy;

    public MyCorsPolicyAttribute()
    {
        // Create a CORS policy.
        _policy = new CorsPolicy
        {
            AllowAnyMethod = true,
            AllowAnyHeader = true
        };

        // Add allowed origins.
        _policy.Origins.Add("http://myclient.azurewebsites.net");
        _policy.Origins.Add("http://www.contoso.com");
    }

    public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request)
    {
        return Task.FromResult(_policy);
    }
}

Vous pouvez maintenant appliquer l’attribut à n’importe quel emplacement que vous placez [EnableCors].

[MyCorsPolicy]
public class TestController : ApiController
{
    .. //

Par exemple, un fournisseur de stratégie CORS personnalisé peut lire les paramètres d’un fichier de configuration.

En guise d’alternative à l’utilisation d’attributs, vous pouvez inscrire un objet ICorsPolicyProviderFactory qui crée des objets ICorsPolicyProvider .

public class CorsPolicyFactory : ICorsPolicyProviderFactory
{
    ICorsPolicyProvider _provider = new MyCorsPolicyProvider();

    public ICorsPolicyProvider GetCorsPolicyProvider(HttpRequestMessage request)
    {
        return _provider;
    }
}

Pour définir ICorsPolicyProviderFactory, appelez la méthode d’extension SetCorsPolicyProviderFactory au démarrage, comme suit :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.SetCorsPolicyProviderFactory(new CorsPolicyFactory());
        config.EnableCors();

        // ...
    }
}

Prise en charge des navigateurs

Le package CORS de l’API web est une technologie côté serveur. Le navigateur de l’utilisateur doit également prendre en charge CORS. Heureusement, les versions actuelles de tous les principaux navigateurs incluent la prise en charge de CORS.