Включение запросов между источниками в веб-API ASP.NET 2

Майк Уосон

Это содержимое предназначено для предыдущей версии .NET. Новая разработка должна использовать ASP.NET Core. Дополнительные сведения об использовании веб-API и запросов между источниками (CORS) в ASP.NET Core см. в разделе:

Параметры безопасности веб-браузера предотвращают отправку запросов AJAX с веб-страницы к другому домену. Такое ограничение называется политикой одного источника. Эта политика предотвращает чтение вредоносным сайтом конфиденциальных данных с другого сайта. Однако иногда может потребоваться разрешить другим сайтам вызывать ваш веб-API.

Общий доступ к ресурсам независимо от источника (CORS) — это стандарт W3C, который позволяет серверу ослабить политику того же источника. С помощью CORS сервер может явным образом разрешить некоторые запросы независимо от источника, а другие — отклонить. CORS является более безопасным и гибким, чем более ранние методы, такие как JSONP. В этом руководстве показано, как включить CORS в приложении веб-API.

Программное обеспечение, используемое в этом руководстве

Введение

В этом руководстве демонстрируется поддержка CORS в веб-API ASP.NET. Начнем с создания двух ASP.NET проектов: один называется "WebService", в котором размещается контроллер веб-API, а другой — "WebClient", который вызывает WebService. Так как два приложения размещаются в разных доменах, запрос AJAX от WebClient к WebService является запросом между источниками.

Отображение веб-службы и веб-клиента

Что такое "одно и то же происхождение"?

Два URL-адреса имеют одинаковый источник, если они имеют одинаковые схемы, узлы и порты. (RFC 6454)

Эти два URL-адреса имеют одинаковый источник:

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

Эти URL-адреса имеют разные источники, чем два предыдущих:

  • http://example.net — другой домен
  • http://example.com:9000/foo.html — другой порт
  • https://example.com/foo.html — другая схема
  • http://www.example.com/foo.html — другой поддомен

Примечание

Интернет-Обозреватель не учитывает порт при сравнении источников.

Создание проекта WebService

Примечание

В этом разделе предполагается, что вы уже знаете, как создавать проекты веб-API. Если нет, см. раздел начало работы с веб-API ASP.NET.

  1. Запустите Visual Studio и создайте проект веб-приложения ASP.NET (платформа .NET Framework).

  2. В диалоговом окне Новое веб-приложение ASP.NET выберите пустой шаблон проекта. В разделе Добавление папок и основных ссылок для установите флажок Веб-API .

    Диалоговое окно создания проекта ASP.NET в Visual Studio

  3. Добавьте контроллер TestController веб-API со следующим кодом:

    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. Приложение можно запустить локально или развернуть в Azure. (Для снимков экрана в этом руководстве приложение развертывается в Служба приложений Azure веб-приложения.) Чтобы убедиться, что веб-API работает, перейдите по адресу http://hostname/api/test/, где hostname — это домен, в котором развернуто приложение. Вы увидите текст ответа "GET: тестовое сообщение".

    В веб-браузере отображается тестовое сообщение

Создание проекта WebClient

  1. Создайте другой проект веб-приложения ASP.NET (платформа .NET Framework) и выберите шаблон проекта MVC. При необходимости выберите Изменить проверку подлинности>без проверки подлинности. Для работы с этим руководством проверка подлинности не требуется.

    Шаблон MVC в диалоговом окне Создания проекта ASP.NET в Visual Studio

  2. В Обозреватель решений откройте файл Views/Home/Index.cshtml. Замените код в этом файле следующим кодом:

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

    Для переменной serviceUrl используйте универсальный код ресурса (URI) приложения WebService.

  3. Запустите приложение WebClient локально или опубликуйте его на другом веб-сайте.

При нажатии кнопки "Попробовать" запрос AJAX отправляется в приложение WebService с помощью метода HTTP, указанного в раскрывающемся списке (GET, POST или PUT). Это позволяет изучить различные запросы между источниками. В настоящее время приложение WebService не поддерживает CORS, поэтому при нажатии кнопки появится сообщение об ошибке.

Ошибка

Примечание

Если вы watch HTTP-трафик в таком инструменте, как Fiddler, вы увидите, что браузер отправляет запрос GET, и запрос будет выполнен успешно, но вызов AJAX возвращает ошибку. Важно понимать, что политика одного источника не запрещает браузеру отправлять запрос. Вместо этого приложение не видит ответ.

Веб-отладчик Fiddler с веб-запросами

Включение CORS

Теперь давайте включим CORS в приложении WebService. Сначала добавьте пакет NuGet CORS. В Visual Studio в меню Сервис выберите Диспетчер пакетов NuGet, а затем — Консоль диспетчера пакетов. В окне Консоль диспетчера пакетов введите следующую команду:

Install-Package Microsoft.AspNet.WebApi.Cors

Эта команда устанавливает последнюю версию пакета и обновляет все зависимости, включая основные библиотеки веб-API. Используйте флаг для -Version определенной версии. Для пакета CORS требуется веб-API 2.0 или более поздней версии.

Откройте файл App_Start/WebApiConfig.cs. Добавьте следующий код в метод 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 }
            );
        }
    }
}

Затем добавьте атрибут [EnableCors] в 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...
    }
}

Для параметра origins используйте универсальный код ресурса (URI), в котором развернуто приложение WebClient. Это позволяет выполнять междоменные запросы из WebClient, но по-прежнему запрещает все остальные междоменные запросы. Далее я подробно опишу параметры для [EnableCors] .

Не включайте косую черту в конце URL-адреса источника .

Повторно разверните обновленное приложение WebService. Вам не нужно обновлять WebClient. Теперь запрос AJAX от WebClient должен завершиться успешно. Все методы GET, PUT и POST разрешены.

Веб-браузер с сообщением об успешном тестировании

Принцип работы CORS

В этом разделе описывается, что происходит в запросе CORS на уровне HTTP-сообщений. Важно понимать, как работает CORS, чтобы можно было правильно настроить атрибут [EnableCors] и устранить неполадки, если все работает неправильно.

В спецификации CORS представлено несколько новых заголовков HTTP, которые позволяют выполнять запросы между источниками. Если браузер поддерживает CORS, он автоматически задает эти заголовки для запросов между источниками; Вам не нужно выполнять никаких специальных действий в коде JavaScript.

Ниже приведен пример запроса между источниками. Заголовок Origin предоставляет домен сайта, выполняющего запрос.

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

Если сервер разрешает запрос, он задает заголовок Access-Control-Allow-Origin. Значение этого заголовка либо соответствует заголовку Origin, либо является подстановочным знаком "*", что означает, что любой источник разрешен.

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

Если ответ не содержит заголовок Access-Control-Allow-Origin, запрос AJAX завершится ошибкой. В частности, браузер запрещает запрос. Даже если сервер возвращает успешный ответ, браузер не делает ответ доступным для клиентского приложения.

Предварительные запросы

Для некоторых запросов CORS браузер отправляет дополнительный запрос, называемый "предварительным запросом", перед отправкой фактического запроса на ресурс.

Браузер может пропустить предварительный запрос, если выполняются следующие условия:

  • Метод запроса— GET, HEAD или POST, а также

  • Приложение не задает заголовки запросов, кроме Accept, Accept-Language, Content-Language, Content-Type или Last-Event-ID, и

  • Заголовок Content-Type (если задан) имеет один из следующих вариантов:

    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

Правило о заголовках запросов применяется к заголовкам, которые приложение задает путем вызова setRequestHeader в объекте XMLHttpRequest . (Спецификация CORS называет эти заголовки запросов на создание.) Правило не применяется к заголовкам, которые может задавать браузер , например User-Agent, Host или Content-Length.

Ниже приведен пример предварительного запроса:

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

В предварительном запросе используется метод HTTP OPTIONS. Он включает два специальных заголовка:

  • Access-Control-Request-Method: метод HTTP, который будет использоваться для фактического запроса.
  • Access-Control-Request-Headers: список заголовков запросов, заданных приложением для фактического запроса. (Опять же, это не включает заголовки, которые задается браузером.)

Ниже приведен пример ответа, предполагающего, что сервер разрешает запрос:

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

Ответ содержит заголовок Access-Control-Allow-Methods, в котором перечислены разрешенные методы, и, при необходимости, заголовок Access-Control-Allow-Headers, в котором перечислены разрешенные заголовки. Если предварительный запрос выполнен успешно, браузер отправляет фактический запрос, как описано выше.

Средства, обычно используемые для тестирования конечных точек с предварительными запросами OPTIONS (например, Fiddler и Postman), по умолчанию не отправляют необходимые заголовки OPTIONS. Убедитесь, что заголовки Access-Control-Request-Method и Access-Control-Request-Headers отправляются вместе с запросом, а заголовки OPTIONS достигают приложения через IIS.

Чтобы разрешить приложению ASP.NET получать и обрабатывать запросы OPTION, добавьте следующую конфигурацию в файл web.config приложения в <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>

Удаление OPTIONSVerbHandler не позволяет службам IIS обрабатывать запросы OPTIONS. Замена позволяет запросам ExtensionlessUrlHandler-Integrated-4.0 OPTIONS обращаться к приложению, так как при регистрации модуля по умолчанию разрешены только запросы GET, HEAD, POST и DEBUG с URL-адресами без расширения.

Правила области для [EnableCors]

Вы можете включить CORS для каждого действия, контроллера или глобально для всех контроллеров веб-API в приложении.

За действие

Чтобы включить CORS для одного действия, задайте атрибут [EnableCors] в методе действия. В следующем примере cors включается только для 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) { ... }
}

На контроллер

Если вы задали [EnableCors] в классе контроллера, он применяется ко всем действиям на контроллере. Чтобы отключить CORS для действия, добавьте в действие атрибут [DisableCors] . В следующем примере cors включается для каждого метода, кроме 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) { ... }
}

Глобально

Чтобы включить CORS для всех контроллеров веб-API в приложении, передайте экземпляр EnableCorsAttribute в метод EnableCors :

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

Если для атрибута задано несколько область, приоритет будет следующим:

  1. Действие
  2. Контроллер
  3. Глобальный

Установка разрешенных источников

Параметр origins атрибута [EnableCors] указывает, каким источникам разрешен доступ к ресурсу. Значение представляет собой разделенный запятыми список разрешенных источников.

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

Можно также использовать подстановочный знак "*", чтобы разрешить запросы из любых источников.

Тщательно продумайте, прежде чем разрешать запросы из любого источника. Это означает, что буквально любой веб-сайт может выполнять вызовы AJAX к веб-API.

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

Настройка разрешенных методов HTTP

Параметр methods атрибута [EnableCors] указывает, каким методам HTTP разрешен доступ к ресурсу. Чтобы разрешить все методы, используйте подстановочное значение "*". В следующем примере разрешены только запросы GET и POST.

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

Настройка разрешенных заголовков запросов

В этой статье ранее было описано, как предварительный запрос может включать заголовок Access-Control-Request-Headers, перечисляя заголовки HTTP, заданные приложением (так называемые "заголовки запросов автора"). Параметр headers атрибута [EnableCors] указывает, какие заголовки запросов автора разрешены. Чтобы разрешить любые заголовки, задайте для заголовков значение "*". Чтобы разрешить определенные заголовки, задайте для заголовков разделенный запятыми список разрешенных заголовков:

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

Однако браузеры не совсем согласованы в том, как они задают Access-Control-Request-Headers. Например, в настоящее время Chrome включает "origin". FireFox не включает стандартные заголовки, такие как "Accept", даже если приложение задает их в скрипте.

Если для заголовков задано значение, отличное от "*", необходимо включить по крайней мере "accept", "content-type" и "origin" и все пользовательские заголовки, которые вы хотите поддерживать.

Настройка разрешенных заголовков ответов

По умолчанию браузер не предоставляет приложению все заголовки ответов. Заголовки ответов, доступные по умолчанию:

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

Спецификация CORS вызывает эти простые заголовки ответов. Чтобы сделать другие заголовки доступными для приложения, задайте параметр exposedHeaders[EnableCors].

В следующем примере метод контроллера Get задает пользовательский заголовок с именем "X-Custom-Header". По умолчанию браузер не предоставляет этот заголовок в запросе независимо от источника. Чтобы сделать заголовок доступным, включите X-Custom-Header в 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;
    }
}

Передача учетных данных в запросах независимо от источника

Учетные данные требуют специальной обработки в запросе CORS. По умолчанию браузер не отправляет учетные данные с запросом из разных источников. Учетные данные включают файлы cookie, а также схемы проверки подлинности HTTP. Чтобы отправить учетные данные с запросом независимо от источника, клиент должен задать для XMLHttpRequest.withCredentials значение true.

Использование XMLHttpRequest напрямую:

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

В jQuery:

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

Кроме того, сервер должен разрешить учетные данные. Чтобы разрешить учетные данные независимо от источника в веб-API, задайте для свойства SupportsCredentials значение true в атрибуте [EnableCors] :

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

Если это свойство имеет значение true, HTTP-ответ будет содержать заголовок Access-Control-Allow-Credentials. Этот заголовок сообщает браузеру, что сервер разрешает учетные данные для запроса независимо от источника.

Если браузер отправляет учетные данные, но ответ не содержит допустимый заголовок Access-Control-Allow-Credentials, браузер не предоставит ответ приложению, и запрос AJAX завершится ошибкой.

Будьте внимательны при установке параметра SupportsCredentials в значение true, так как это означает, что веб-сайт в другом домене может отправлять учетные данные пользователя, выполнившего вход, в веб-API от имени пользователя, без уведомления пользователя. В спецификации CORS также указано, что задание для источников значения "*" недопустимо, если свойство SupportsCredentials имеет значение true.

Настраиваемые поставщики политик CORS

Атрибут [EnableCors] реализует интерфейс ICorsPolicyProvider . Вы можете предоставить собственную реализацию, создав класс, производный от Attribute и реализующий 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);
    }
}

Теперь вы можете применить атрибут в любом месте, в которое вы поместите [EnableCors].

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

Например, пользовательский поставщик политики CORS может считывать параметры из файла конфигурации.

В качестве альтернативы использованию атрибутов можно зарегистрировать объект ICorsPolicyProviderFactory , который создает объекты ICorsPolicyProvider .

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

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

Чтобы задать ICorsPolicyProviderFactory, вызовите метод расширения SetCorsPolicyProviderFactory при запуске следующим образом:

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

        // ...
    }
}

Поддержка браузеров

Пакет CORS веб-API — это технология на стороне сервера. Браузер пользователя также должен поддерживать CORS. К счастью, в текущих версиях всех основных браузеров включена поддержка CORS.