Подтвердить что ты не робот

Обработка запросов CORS для предпросмотра действий ASP.NET MVC

Я пытаюсь выполнить кросс-доменный запрос POST для действия контроллера ASP.NET MVC. Это действие контроллера принимает и использует различные параметры. Проблема заключается в том, что при выполнении запроса предварительной проверки действие контроллера фактически пытается выполнить, и поскольку запрос OPTIONS не передает никаких данных, действие контроллера выдает 500 HTTP-ошибок. Если я удалю код, который использует этот параметр, или сам параметр, вся цепочка запросов будет успешно завершена.

Пример того, как это реализовано:

Действие контроллера

public ActionResult GetData(string data)
{
    return new JsonResult
    {
        Data = data.ToUpper(),
        JsonRequestBehavior = JsonRequestBehavior.AllowGet
    };
}

Клиентский код

<script type="text/javascript">
        $(function () {
            $("#button-request").click(function () {
                var ajaxConfig = {
                    dataType: "json",
                    url: "http://localhost:8100/host/getdata",
                    contentType: 'application/json',
                    data: JSON.stringify({ data: "A string of data" }),
                    type: "POST",
                    success: function (result) {
                        alert(result);
                    },
                    error: function (jqXHR, textStatus, errorThrown) {
                        alert('Error: Status: ' + textStatus + ', Message: ' + errorThrown);
                    }
                };

                $.ajax(ajaxConfig);
            });
        });
    </script>

Теперь, всякий раз, когда выполняется запрос предварительной проверки, он возвращает 500 HTTP-код, потому что параметр "data" равен NULL, поскольку запрос OPTIONS не передает никаких значений.

Серверное приложение было настроено в моем локальном IIS на порту 8100, а страница, на которой выполняется клиентский код, настроена на порт 8200, чтобы имитировать междоменные вызовы.

Я также настроил хост (на 8100) со следующими заголовками:

Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Origin: http://localhost:8200

Обходной путь, который я нашел, состоял в том, чтобы проверить HTTP-метод, который выполняет действие, и если это запрос OPTIONS, чтобы просто вернуть пустой контент, в противном случае выполните код действия. Например:

public ActionResult GetData(string data)
{
    if (Request.HttpMethod == "OPTIONS") {
        return new ContentResult();
    } else {
        return new JsonResult
        {
            Data = data.ToUpper(),
            JsonRequestBehavior = JsonRequestBehavior.AllowGet
        };
    }
}

Но этот подход кажется мне очень неуклюжим. Я подумал о добавлении этой логики к Attribute, но даже это означало бы украшение каждого действия, которое будет вызвано с помощью CORS с ним.

Есть ли более элегантное решение для работы этой функции?

4b9b3361

Ответ 1

Итак, я нашел решение, которое работает. Для каждого запроса я проверяю, является ли это запросом CORS и поступает ли запрос с глаголом OPTIONS, указывая, что это предварительный запрос. Если это так, я просто отправляю пустой ответ (который, конечно, содержит только заголовки, настроенные в IIS), тем самым сводя на нет выполнение действия контроллера.

Затем, если клиент подтверждает, что ему разрешено выполнить запрос на основе возвращенных заголовков из предварительной проверки, выполняется фактическая проверка POST и выполняется действие контроллера. И пример моего кода:

protected void Application_BeginRequest()
{
    if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
        Request.HttpMethod == "OPTIONS") {
        Response.Flush();
    }
}

Как уже упоминалось, это работало для меня, но если кто-то знает о лучшем способе или о каких-либо недостатках в моей текущей реализации, я был бы рад услышать о них.

Ответ 2

расширяя ответ на Carl, я взял его код и подключил его к моему конвейеру OWIN:

app.Use((context, next) =>
{
     if (context.Request.Headers.Any(k => k.Key.Contains("Origin")) && context.Request.Method == "OPTIONS")
     {
         context.Response.StatusCode = 200;
         return context.Response.WriteAsync("handled");
     }

     return next.Invoke();
});

Просто добавьте это в начало (или где-нибудь до того, как вы зарегистрируете WebAPI) своего IAppBuilder в Startup.cs

Ответ 3

Принятый ответ работает как шарм, но я обнаружил, что запрос фактически передавался контроллеру. Я получал код состояния 200, но тело ответа содержало много HTML с исключением из контроллера. Поэтому вместо использования Response.Flush() я обнаружил, что лучше использовать Response.End(), что останавливает выполнение запроса. Это альтернативное решение будет выглядеть так:

РЕДАКТИРОВАТЬ: зафиксирована опечатка, выполненная из исходного ответа.

protected void Application_BeginRequest()
{
    if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
        Request.HttpMethod == "OPTIONS") {
        Response.End();
    }
}

Ответ 4

Вот как я обработал проблемы перед полетом /CORS с помощью ASP.Net Web Api. Я просто добавил пакет Microsoft.AspNet.WebApi.Cors Nuget в свой веб-проект. Затем в моем файле WebApiConfig.cs я добавил эту строку:

config.EnableCors(new ApplicationCorsPolicy());

и создал пользовательский класс PolicyProvider

public class ApplicationCorsPolicy : Attribute, ICorsPolicyProvider
{
    public async Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var corsRequestContext = request.GetCorsRequestContext();
        var originRequested = corsRequestContext.Origin;

        if (await IsOriginFromAPaidCustomer(originRequested))
        {
            // Grant CORS request
            var policy = new CorsPolicy
            {
                AllowAnyHeader = true,
                AllowAnyMethod = true
            };
            policy.Origins.Add(originRequested);
            return policy;
        }
        // Reject CORS request
        return null;
    }

    private async Task<bool> IsOriginFromAPaidCustomer(string originRequested)
    {
        // Do database look up here to determine if origin should be allowed.
        // In my application I have a table that has a list of domains that are
        // allowed to make API requests to my service. This is validated here.
        return true;
    }
}

Посмотрите, структура Cors позволяет вам добавить свою собственную логику для определения того, какие источники разрешены, и т.д. Это очень полезно, если вы публикуете REST API для внешнего мира и список людей (источников), которые могут получить доступ ваш сайт находится в контролируемой среде, такой как база данных. Теперь, если вы просто разрешаете все происхождение (что может быть не очень хорошая идея во всех случаях), вы можете просто сделать это в WebApiConfig.cs, чтобы включить CORS глобально:

config.EnableCors();

Подобно фильтрам и обработчикам в WebApi, вы также можете добавлять аннотации уровня или метода к вашим контроллерам, например:

[EnableCors("*, *, *, *")]

Обратите внимание, что атрибут EnableCors имеет конструктор, который принимает следующие параметры

  • Список разрешенных источников
  • Список разрешенных заголовков запросов
  • Список разрешенных методов HTTP
  • Список разрешенных заголовков ответов

Вы можете указать статически на каждом контроллере/конечной точке, которому разрешен доступ к какому ресурсу.

Обновление 06/24/2016: Я должен упомянуть, что в моем Web.config есть следующее. Похоже, что это не могут быть значения по умолчанию для всех.

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

Источник: Microsoft

Ответ 5

Ни один из этих ответов не работал у меня, но следующие настройки webconfig. Две настройки для меня устанавливали Access-Control-Allow-Headers на Content-Type и комментировали строку, которая удаляет OPTIONSVerbHandler:

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"></modules>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type" />
      </customHeaders>
    </httpProtocol>
    <handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <!--<remove name="OPTIONSVerbHandler" />-->
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
  </system.webServer>

Ответ 6

Это может быть красная селедка. Недавно я получил CORS, отлично работая, не пробираясь ни по одному из обручей, которые вы делаете.

Это было сделано с использованием комбинации Thinktecture.IdentityModel nuget и, что еще важнее... СНЯТИЕ всех ссылок на WebDAV. Это включает удаление модуля webdav из IIS и обеспечение следующих строк в вашей веб-конфигурации:

<system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="WebDAVModule" />
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules>
    <handlers>
      <remove name="WebDAV" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

Затем вы можете просто использовать thinktecture, чтобы настроить CORS из вашего Global.asax, используя статический класс следующим образом:

public class CorsConfig
{
    public static void RegisterCors(HttpConfiguration httpConfiguration)
    {
        var corsConfig = new WebApiCorsConfiguration();
        corsConfig.RegisterGlobal(httpConfiguration);

        corsConfig.ForAllResources().AllowAllOriginsAllMethodsAndAllRequestHeaders();
    }
}

ИСТОЧНИК: http://brockallen.com/2012/06/28/cors-support-in-webapi-mvc-and-iis-with-thinktecture-identitymodel/

Ответ 7

На самом деле, я только что "упорядочил" принятый ответ

Не стесняйтесь поправлять меня

protected void Application_BeginRequest()//Put it inside global.asax[you might also need, inside web.config:<modules runAllManagedModulesForAllRequests="true">]
    {
        if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
            Request.HttpMethod == "OPTIONS")
        {
            Response.Headers.Set("Allow", "GET, POST");//normally you only need  get and post http method but you can append others also
            Response.Headers.Set("Access-Control-Allow-Origin", "*");//its advised to add only the urls(i think its comma seperated)(https://example.com,https://*.example.com) you want to allow here
            Response.Headers.Set("Access-Control-Allow-Headers", "*");//put here those Headers that you are going to need to pass(like ContentType) in the frontend
            Response.End();
        }
    }