Activer les requêtes inter-origines 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 requêtes cross-origin (CORS) dans ASP.NET Core, consultez :
- Tutoriel : Création d’une API web avec ASP.NET Core
- Activation des demandes multi-origines (CORS) dans ASP.NET Core
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.
Cross Origin Resource Sharing (CORS) est une norme W3C qui permet à un serveur de détendre 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.
Logiciel utilisé dans le tutoriel
- Visual Studio
- API web 2.2
Introduction
Ce tutoriel illustre la prise en charge de CORS dans API Web ASP.NET. Nous allons commencer par créer deux projets ASP.NET : 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.
Qu’est-ce que la « même origine » ?
Deux URL ont la même origine s’ils 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érenthttp://example.com:9000/foo.html
: Port différenthttps://example.com/foo.html
: Schéma différenthttp://www.example.com/foo.html
: Sous-domaine différent
Remarque
Internet Explorer ne prend pas en compte le port lors de la comparaison des origines.
Créer le projet WebService
Remarque
Cette section part du principe que vous savez déjà comment créer des projets d’API web. Si ce n’est pas le cas, consultez Prise en main de API Web ASP.NET.
Démarrez Visual Studio et créez un projet d’application web (.NET Framework) ASP.NET.
Dans la boîte de dialogue Nouvelle ASP.NET application web, sélectionnez le modèle de projet vide . Sous Ajouter des dossiers et des références principales, cochez la case API web.
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") }; } } }
Vous pouvez exécuter l’application localement ou le 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ù le nom d’hôte est le domaine où vous avez déployé l’application. Le texte de la réponse doit s’afficher : « GET : Test Message ».
Créer le projet WebClient
Créez un autre projet d’application web (.NET Framework) ASP.NET 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 didacticiel.
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.
Exécutez l’application WebClient localement ou publiez-la sur un autre site web.
Lorsque vous cliquez sur le bouton « Essayer », une requête 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.
Remarque
Si vous regardez le trafic HTTP dans un outil tel que 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 requête. Au lieu de cela, elle empêche l’application de voir la réponse.
Activez CORS
Nous allons maintenant activer CORS dans l’application WebService. Tout d’abord, ajoutez le package NuGet CORS. Dans Visual Studio, dans le menu Outils, sélectionnez NuGet Gestionnaire de package, puis Gestionnaire de package Console. Dans la fenêtre Gestionnaire de package console, tapez la commande suivante :
Install-Package Microsoft.AspNet.WebApi.Cors
Cette commande installe le package le plus récent 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 une version 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 d’origine, utilisez l’URI où vous avez déployé l’application WebClient. Cela permet aux requêtes inter-origines de WebClient, tout en désallouant toutes les autres requêtes inter-domaines. Plus tard, je vais décrire les paramètres pour [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.
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 requêtes 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 requête cross-origin. L’en-tête « Origine » donne le domaine du site qui effectue la requête.
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 met pas la réponse à la disposition de l’application cliente.
Demandes préliminaires
Pour certaines requêtes CORS, le navigateur envoie une requête supplémentaire, appelée « demande préliminaire », avant d’envoyer la requête réelle pour la ressource.
Le navigateur peut ignorer la demande 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 aucun en-tête de requête autre 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
- text/plain
La règle relative aux en-têtes de requête s’applique aux en-têtes définis par l’application en appelant setRequestHeader sur l’objet XMLHttpRequest . (La spécification CORS appelle ces « en-têtes de demande 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 préliminaire :
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 requête 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 d’en-têtes de requête définis par l’application sur la requête réelle. (Là encore, cela n’inclut pas d’en-têtes que le navigateur définit.)
Voici un exemple de réponse, en supposant que le serveur autorise la requête :
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 préliminaire réussit, le navigateur envoie la requête réelle, comme décrit précédemment.
Les outils couramment utilisés pour tester les points de terminaison avec des requêtes OPTIONS préliminaires n’envoient pas les en-têtes OPTIONS requis par défaut. Vérifiez que les en-têtes et Access-Control-Request-Headers
les Access-Control-Request-Method
en-têtes sont envoyés avec la requête et que les en-têtes OPTIONS atteignent l’application via IIS.
Pour configurer IIS afin d’autoriser une application ASP.NET à recevoir et 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 d’IIS OPTIONSVerbHandler
empêche IIS de gérer les demandes OPTIONS. Le remplacement des ExtensionlessUrlHandler-Integrated-4.0
demandes OPTIONS permet d’atteindre 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 seule action, définissez l’attribut [EnableCors] sur la méthode d’action. L’exemple suivant active CORS uniquement pour la GetItem
méthode.
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 de contrôleur, elle 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, sauf PutItem
.
[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, transmettez une 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 à plusieurs étendues, l’ordre de priorité est :
- Action
- Contrôleur
- Global
Définir les origines autorisées
Le paramètre d’origine 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 requêtes provenant de n’importe quelle origine.
Envisagez 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 de méthodes 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 demande autorisés
Cet article décrit précédemment comment une demande préliminaire 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 demande d’auteur »). Le paramètre d’en-têtes de l’attribut [EnableCors] spécifie les en-têtes de demande d’auteur autorisés. Pour autoriser tous 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 « Accepter », même lorsque l’application les définit dans le script.
Si vous définissez des en-têtes sur quelque chose d’autre que « * », vous devez inclure au moins « accepter », « content-type » et « origin », ainsi que 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
- Type de contenu
- Expire
- 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 requête cross-origin. 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;
}
}
Transmettre les informations d’identification dans les demandes d’origine croisée
Les informations d’identification nécessitent une gestion spéciale dans une demande CORS. Par défaut, le navigateur n’envoie pas d’informations d’identification avec une requête cross-origin. Les informations d’identification incluent des cookies ainsi que des schémas d’authentification HTTP. Pour envoyer des informations d’identification avec une requête cross-origin, 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 requête AJAX échoue.
Veillez à définir SupportsCredentials sur true, car cela signifie qu’un site web sur un autre domaine peut envoyer les informations d’identification d’un utilisateur connecté à votre API web au nom de l’utilisateur, sans que l’utilisateur ne sache. 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 à partir 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 navigateurs principaux incluent la prise en charge de CORS.