Habilitación de solicitudes entre orígenes en ASP.NET Web API 2
Por Mike Wasson
Este contenido corresponde a una versión anterior de .NET. El nuevo desarrollo debería usar ASP.NET Core. Para más información sobre el uso de la API web y las solicitudes entre orígenes (CORS) en ASP.NET Core, consulte:
- Tutorial: Creación de una API web con ASP.NET Core
- Habilitar solicitudes entre orígenes (CORS) en ASP.NET Core
La seguridad del explorador impide que una página web realice solicitudes AJAX a otro dominio. Esta restricción se conoce como directiva de mismo origen y evita que un sitio malintencionado lea información confidencial de otro sitio. Sin embargo, a veces es posible que quiera permitir que otros llamen a su API web.
Uso compartido de recursos entre orígenes (CORS) es una norma de W3C que permite mayor flexibilidad a los servidores en la directiva de mismo origen. Con CORS, un servidor puede permitir explícitamente algunas solicitudes de origen cruzado y rechazar otras. CORS es más seguro y más flexible que las técnicas anteriores, como JSONP. En este tutorial se muestra cómo habilitar CORS en su aplicación de API web.
Software utilizado en este tutorial
- Visual Studio
- API Web 2.2
Introducción
En este tutorial se muestra la compatibilidad con CORS en ASP.NET API web. Comenzaremos creando dos proyectos de ASP.NET, uno denominado "WebService", que hospeda un controlador de API web y el otro denominado "WebClient", que llama a WebService. Dado que las dos aplicaciones se hospedan en dominios diferentes, una solicitud AJAX de WebClient a WebService es una solicitud entre orígenes.
¿Qué es "mismo origen"?
Dos URL tienen el mismo origen si tienen esquemas, hosts y puertos idénticos. (RFC 6454)
Estas dos direcciones URL tienen el mismo origen:
http://example.com/foo.html
http://example.com/bar.html
Estas direcciones URL tienen orígenes diferentes a las dos anteriores:
http://example.net
- Dominio diferentehttp://example.com:9000/foo.html
- Puerto diferentehttps://example.com/foo.html
- Esquema diferentehttp://www.example.com/foo.html
- Subdominio diferente
Nota:
Internet Explorer no tiene en cuenta el puerto al comparar orígenes.
Creación del proyecto WebService
Nota:
En esta sección se supone que ya sabe cómo crear proyectos de API web. Si no, consulte Introducción a ASP.NET Web API.
Inicie Visual Studio y cree un nuevo proyecto de Aplicación web de ASP.NET (.NET Framework).
En el cuadro de diálogo Nueva ASP.NET aplicación web, seleccione la plantilla de proyecto Vacío. En Agregar carpetas y referencias principales para, active la casilla API web.
Agregue un controlador de API web denominado
TestController
con el siguiente código: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") }; } } }
Puede ejecutar la aplicación localmente o implementarla en Azure. (Para las capturas de pantalla de este tutorial, la aplicación recurre a Azure App Service Web Apps). Para comprobar que la API web funciona, vaya a
http://hostname/api/test/
, donde nombre de host es el dominio donde implementó la aplicación. Debería ver el texto de respuesta "GET: Test Message".
Creación del proyecto WebClient
Cree otro proyecto Aplicación web ASP.NET (.NET Framework) y seleccione la plantilla de proyecto MVC. Seleccione (opcionalmente) Cambiar autenticación>Sin autenticación. No necesita autenticación para este tutorial.
En el Explorador de soluciones, abra el archivo Views/Home/Index.cshtml. Reemplace el código del archivo con el siguiente:
<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> }
Para la variable serviceUrl, use el URI de la aplicación WebService.
Ejecute la aplicación WebClient localmente o publíquela en otro sitio web.
Al hacer clic en el botón "Probar", se envía una solicitud AJAX a la aplicación WebService mediante el método HTTP que aparece en el cuadro desplegable (GET, POST o PUT). Esto le permite examinar diferentes solicitudes entre orígenes. Actualmente, la aplicación WebService no admite CORS, por lo que si hace clic en el botón obtendrá un error.
Nota:
Si observa el tráfico HTTP en una herramienta como Fiddler, verá que el explorador envía la solicitud GET y que la solicitud se realiza correctamente, pero la llamada AJAX devuelve un error. Es importante comprender que la directiva del mismo origen no impide al explorador enviar la solicitud. En su lugar, impide que la aplicación vea la respuesta.
Habilitación de CORS
Es hora de habilitar CORS en la aplicación WebService. Primero, agregue el paquete NuGet CORS. En Visual Studio, en el menú Herramientas, seleccione Administrador de paquetes NuGet, después seleccione Consola del administrador de paquetes. En la ventana de la consola del administrador de paquetes, escriba el comando siguiente:
Install-Package Microsoft.AspNet.WebApi.Cors
Este comando instala el paquete más reciente y actualiza todas las dependencias, incluidas las bibliotecas principales de API web. Use la marca -Version
para establecer una versión específica como destino. El paquete CORS requiere API Web 2.0 o posterior.
Abra el archivo App_Start/WebApiConfig.cs. Agregue el código siguiente al método 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 }
);
}
}
}
A continuación, agregue el atributo [EnableCors] a la clase TestController
:
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...
}
}
Para el parámetro orígenes, use el URI donde implementó la aplicación WebClient. Esto permite solicitudes entre orígenes de WebClient, a la vez que no permite todas las demás solicitudes entre dominios. Más adelante, se describirán los parámetros de [EnableCors] con más detalle.
No incluya una barra diagonal al final de la URL de orígenes.
Vuelva a implementar la aplicación WebService actualizada. No es necesario actualizar WebClient. Ahora la solicitud AJAX de WebClient debería tener éxito. Todos los métodos GET, PUT y POST están permitidos.
Cómo funciona CORS
Esta sección describe lo que sucede en una solicitud CORS a nivel de los mensajes HTTP. Es importante comprender cómo funciona CORS, de modo que pueda configurar el atributo [EnableCors] correctamente y solucionar problemas si las cosas no funcionan como se espera.
La especificación CORS introdujo varios encabezados HTTP nuevos que permiten solicitudes entre orígenes. Si un explorador admite CORS, este establece estos encabezados automáticamente para las solicitudes entre orígenes. No es necesario hacer nada especial en el código JavaScript.
Este es un ejemplo de una solicitud entre orígenes. El encabezado "Origin" proporciona el dominio del sitio que realiza la solicitud.
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 el servidor permite la solicitud, establece el encabezado Access-Control-Allow-Origin. El valor de este encabezado coincide con el encabezado Origin o es el valor comodín "*", lo que significa que se permite cualquier origen.
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 respuesta no incluye el encabezado Access-Control-Allow-Origin, se produce un error en la solicitud AJAX. En concreto, el navegador no permite la solicitud. Incluso si el servidor devuelve una respuesta correcta, el explorador no hace disponible la respuesta para la aplicación cliente.
Solicitud preparatoria
El explorador envía una solicitud adicional denominada "solicitud preparatoria" para algunas solicitudes CORS antes de enviar la solicitud real para el recurso.
El navegador puede omitir la solicitud preparatoria si se cumplen estas condiciones:
El método de solicitud es GET, HEAD o POST, y
La aplicación no establece ningún encabezado de solicitud distinto de Accept, Accept-Language, Content-Language, Content-Type o Last-Event-ID, y
El encabezado Content-Type (si se establece) es uno de los siguientes:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
La regla sobre los encabezados de solicitud se aplica a los encabezados que establece la aplicación llamando a setRequestHeader en el objeto XMLHttpRequest. (La especificación CORS llama a estos "encabezados de solicitud de autor"). La regla no se aplica a los encabezados que el explorador puede establecer, como User-Agent, Host o Content-Length.
Un ejemplo de una solicitud preparatoria:
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 solicitud preparatoria utiliza el método OPCIONES HTTP. Incluye dos encabezados especiales:
- Access-Control-Request-Method: método HTTP que se usará para la solicitud real.
- Access-Control-Request-Headers: una lista de encabezados de solicitud que la aplicación establece en la solicitud real. (De nuevo, esto no incluye encabezados que establece el explorador).
Esta es una respuesta de ejemplo, suponiendo que el servidor permita la solicitud:
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 respuesta incluye un encabezado Access-Control-Allow-Methods que enumera los métodos permitidos y un encabezado Access-Control-Allow-Headers (opcional) que enumera los encabezados permitidos. Si la solicitud preparatoria se realiza correctamente, el explorador envía la solicitud real, como se describe anteriormente.
Las herramientas que se usan normalmente para probar los puntos de conexión con solicitudes OPTIONS previas no envían los encabezados OPTIONS necesarios de forma predeterminada. Confirme que los encabezados Access-Control-Request-Method
y Access-Control-Request-Headers
se envían con la solicitud y que los encabezados OPTIONS llegan a la aplicación a través de IIS.
Para configurar IIS para permitir que una aplicación de ASP.NET reciba y controle las solicitudes OPTION, agregue la siguiente configuración al archivo web.config de la aplicación en la sección <system.webServer><handlers>
:
<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>
Eliminar OPTIONSVerbHandler
impide que IIS controle las solicitudes OPTIONS. Sustituir ExtensionlessUrlHandler-Integrated-4.0
permite que las solicitudes OPTIONS lleguen a la aplicación porque el registro de módulo predeterminado solo permite solicitudes GET, HEAD, POST y DEBUG con direcciones URL sin extensión.
Reglas de ámbito para [EnableCors]
Puede habilitar CORS por acción, por controlador o globalmente para todos los controladores de API web de su aplicación.
Por acción
Para habilitar CORS para una sola acción, establezca el atributo [EnableCors] en el método de acción. En el ejemplo siguiente se habilita CORS solo para el método GetItem
.
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) { ... }
}
Por controlador
Si establece [EnableCors] en la clase de controlador, se aplica a todas las acciones del controlador. Para deshabilitar CORS para una acción, agregue el atributo [DisableCors] a la acción. En el ejemplo siguiente se habilita CORS para cada método excepto 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) { ... }
}
Globalmente
Para habilitar CORS para todos los controladores de API web de la aplicación, pase una instancia EnableCorsAttribute al método EnableCors:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var cors = new EnableCorsAttribute("www.example.com", "*", "*");
config.EnableCors(cors);
// ...
}
}
Si establece el atributo en más de un ámbito, el orden de prioridad es:
- Action
- Controlador
- Global
Establecimiento de los orígenes permitidos
El parámetro origins del atributo [EnableCors] especifica qué orígenes pueden tener acceso al recurso. El valor es una lista de los orígenes permitidos separada por comas.
[EnableCors(origins: "http://www.contoso.com,http://www.example.com",
headers: "*", methods: "*")]
También puede usar el valor comodín "*" para permitir solicitudes desde cualquier origen.
Piénselo detenidamente antes de permitir solicitudes desde cualquier origen. Significa que literalmente cualquier sitio web puede realizar llamadas AJAX a su API web.
// Allow CORS for all origins. (Caution!)
[EnableCors(origins: "*", headers: "*", methods: "*")]
Establecimiento de los métodos HTTP permitidos
El parámetro methods del atributo [EnableCors] especifica qué métodos HTTP pueden tener acceso al recurso. Para permitir todos los métodos, use el valor comodín "*". En el ejemplo siguiente solo se permiten solicitudes GET y POST.
[EnableCors(origins: "http://www.example.com", headers: "*", methods: "get,post")]
public class TestController : ApiController
{
public HttpResponseMessage Get() { ... }
public HttpResponseMessage Post() { ... }
public HttpResponseMessage Put() { ... }
}
Establecimiento de los encabezados de solicitud permitidos
En este artículo se ha descrito anteriormente cómo una solicitud preparatoria podría incluir un encabezado Access-Control-Request-Headers, enumerando los encabezados HTTP establecidos por la aplicación (los denominados "encabezados de solicitud de autor"). El parámetro headers del atributo [EnableCors] especifica qué encabezados de solicitud de autor se permiten. Para permitir cualquier encabezado, establezca encabezados en "*". Para permitir encabezados específicos, establezca encabezados en una lista separada por comas de los encabezados permitidos:
[EnableCors(origins: "http://example.com",
headers: "accept,content-type,origin,x-my-header", methods: "*")]
Sin embargo, los exploradores no son totalmente coherentes en la forma en de establecer Access-Control-Request-Headers. Por ejemplo, Chrome incluye "origen" actualmente. FireFox no incluye encabezados estándar como "Accept", incluso cuando la aplicación los establece en el script.
Si establece encabezados en algo distinto de "*", debe incluir al menos "accept", "content-type" y "origin", además de los encabezados personalizados que desee admitir.
Establecimiento de los encabezados de respuesta permitidos
De forma predeterminada, el explorador no expone todos los encabezados de respuesta a la aplicación. Los encabezados de respuesta que están disponibles de forma predeterminada son:
- Cache-Control
- Content-Language
- Tipo de contenido
- Caducidad
- Last-Modified
- Pragma
La especificación CORS llama a estos encabezados de respuesta simples. Para que otros encabezados estén disponibles para la aplicación, establezca el parámetro exposedHeaders de [EnableCors].
En el ejemplo siguiente, el método Get
del controlador establece un encabezado personalizado denominado "X-Custom-Header". De forma predeterminada, el explorador no expondrá este encabezado en una solicitud entre orígenes. Para que el encabezado esté disponible, incluya "X-Custom-Header" en 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;
}
}
Pasar credenciales en solicitudes entre orígenes
Las credenciales requieren un control especial en una solicitud CORS. De forma predeterminada, el explorador no envía ninguna credencial con una solicitud entre orígenes. Las credenciales incluyen cookies, así como esquemas de autenticación HTTP. Para enviar credenciales con una solicitud entre orígenes, el cliente debe establecer XMLHttpRequest.withCredentials en true.
Uso directo de XMLHttpRequest:
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;
En jQuery:
$.ajax({
type: 'get',
url: 'http://www.example.com/api/test',
xhrFields: {
withCredentials: true
}
Además, el servidor debe permitir las credenciales. Para permitir credenciales entre orígenes en la API web, establezca la propiedad SupportsCredentials en true en el atributo [EnableCors]:
[EnableCors(origins: "http://myclient.azurewebsites.net", headers: "*",
methods: "*", SupportsCredentials = true)]
Si esta propiedad es true, la respuesta HTTP incluirá un encabezado Access-Control-Allow-Credentials. Este encabezado indica al explorador que el servidor permite credenciales para una solicitud entre orígenes.
Si el explorador envía credenciales, pero la respuesta no incluye un encabezado Access-Control-Allow-Credentials válido, el explorador no expondrá la respuesta a la aplicación y se producirá un error en la solicitud AJAX.
Tenga cuidado al establecer SupportsCredentials en true, ya que significa que un sitio web de otro dominio puede enviar las credenciales de un usuario que ha iniciado sesión a la API web en nombre del usuario, sin que este sea consciente. La especificación CORS también indica que establecer orígenes en "*" no es válido si SupportsCredentials es true.
Proveedores de directivas CORS personalizados
El atributo [EnableCors] implementa la interfaz ICorsPolicyProvider. Puede proporcionar su propia implementación mediante la creación de una clase que derive de Attribute e implemente 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);
}
}
Ahora puede aplicar el atributo en cualquier lugar en donde colocaría [EnableCors].
[MyCorsPolicy]
public class TestController : ApiController
{
.. //
Por ejemplo, un proveedor de directivas CORS personalizado podría leer la configuración de un archivo de configuración.
Como alternativa al uso de atributos, puede registrar un objeto ICorsPolicyProviderFactory que crea objetos ICorsPolicyProvider.
public class CorsPolicyFactory : ICorsPolicyProviderFactory
{
ICorsPolicyProvider _provider = new MyCorsPolicyProvider();
public ICorsPolicyProvider GetCorsPolicyProvider(HttpRequestMessage request)
{
return _provider;
}
}
Para establecer ICorsPolicyProviderFactory, llame al método de extensión SetCorsPolicyProviderFactory en el inicio, como se indica a continuación:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.SetCorsPolicyProviderFactory(new CorsPolicyFactory());
config.EnableCors();
// ...
}
}
Compatibilidad con navegadores
El paquete CORS de la API web es una tecnología del lado servidor. El explorador del usuario también debe admitir CORS. Afortunadamente, las versiones actuales de todos los exploradores principales incluyen compatibilidad con CORS.