Несанкционированная страница входа с возвратом через webapi, а не 401


180

Как настроить проект mvc / webapi таким образом, чтобы метод webapi, вызываемый из вида бритвы, не возвращал страницу входа, если она не авторизована?

Это приложение MVC5, которое также имеет контроллеры WebApi для звонков через JavaScript.

Два метода ниже

[Route("api/home/LatestProblems")]      
[HttpGet()]
public List<vmLatestProblems> LatestProblems()
{
    // Something here
}

[Route("api/home/myLatestProblems")]
[HttpGet()]
[Authorize(Roles = "Member")]
public List<vmLatestProblems> mylatestproblems()
{
   // Something there
}

Вызываются через следующий угловой код:

angular.module('appWorship').controller('latest', 
    ['$scope', '$http', function ($scope,$http) {         
        var urlBase = baseurl + '/api/home/LatestProblems';
        $http.get(urlBase).success(function (data) {
            $scope.data = data;
        }).error(function (data) {
            console.log(data);
        });
        $http.get(baseurl + '/api/home/mylatestproblems')
          .success(function (data) {
            $scope.data2 = data;
        }).error(function (data) {
            console.log(data);
        });  
    }]
);

Поэтому я не вошел в систему, и первый метод успешно возвращает данные. второй метод возвращает (в функции успеха) данные, которые содержат эквивалент страницы входа. то есть то, что вы получили бы в mvc, если бы вы запросили действие контроллера, помеченное [Authorize], и ​​вы не вошли в систему.

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

ОБНОВЛЕНИЕ: Поскольку ни одно из приведенных ниже предложений, похоже, больше не работает (изменения в Identity или WebAPI), я создал необработанный пример на github, который должен проиллюстрировать проблему.

Ответы:


78

Существует две реализации AuthorizeAttribute, и вам необходимо убедиться, что вы ссылаетесь на правильную версию для Web API. Существует System.Web.Http.AuthorizeAttribute, который используется для веб-API, и System.Web.Mvc.AuthorizeAttribute, который используется для контроллеров с представлениями. Http.AuthorizeAttribute вернет ошибку 401, если авторизация не удалась, а Mvc.AuthorizeAttribute перенаправит на страницу входа.

Обновлено 26.11.2013

Похоже, что с MVC 5 все резко изменилось, как указал Брок Аллен в своей статье . Я предполагаю, что конвейер OWIN вступает во владение и вводит некоторое новое поведение. Теперь, когда пользователь не авторизован, возвращается статус 200 со следующей информацией в заголовке HTTP.

X-Responded-JSON: {"status":401,"headers":{"location":"http:\/\/localhost:59540\/Account\/Login?ReturnUrl=%2Fapi%2FTestBasic"}}

Вы можете изменить свою логику на стороне клиента, чтобы проверить эту информацию в заголовке, чтобы определить, как ее обрабатывать, вместо того, чтобы искать состояние 401 в ветви ошибок.

Я попытался переопределить это поведение в пользовательском атрибуте AuthorizeAttribute , установив состояние в ответе в методах OnAuthorization и HandleUnauthorizedRequest .

actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);

Но это не сработало. Новый конвейер должен получить этот ответ позже и изменить его так же, как и раньше. Бросок исключения HttpException также не сработал, так как он просто превратился в статус ошибки 500.

Я протестировал решение Брока Аллена, и оно работало, когда я использовал вызов jQuery ajax. Если это не работает для вас, я думаю, что это потому, что вы используете угловой. Запустите свой тест с Fiddler и посмотрите, есть ли следующее в вашем заголовке.

X-Requested-With: XMLHttpRequest

Если это не так, то это проблема. Я не знаком с angular, но если он позволяет вам вставлять свои собственные значения заголовков, добавьте это в ваши запросы ajax, и он, вероятно, начнет работать.


я думаю, что я использую System.web.http.authorizeattribute, по крайней мере этот webapicontroller не имеет использования для system.web.mvc, и переход к определению атрибута authorize отправляет меня в system.web.http
Тим

Привет @ kevin-junghans полностью запутался здесь. В приведенном выше примере от shiva используется атрибут авторизации mvc, который, конечно же, я не должен применять к действию webapi. Пример от Брока Аллена, похоже, не работает, либо не считает его запросом ajax, когда я перехожу.
Тим

1
Я только что заметил этот ответ (думаю, стекопоток не отправляет уведомления). Я добавил пример github, чтобы проиллюстрировать проблему, и теперь добавил свое исправление в угловые заголовки. Спасибо. Однако кажется неправильным, что в атрибуте authorize нет свойства, которое я могу проверить, или упомянутая вами оригинальная функциональность больше не работает.
Тим

3
Использование POSTMAN и параметра заголовка X-Requested-With: XMLHttpRequest работает для меня ... спасибо
chemitaxis

Итак, что, если у вас есть то, что вы намереваетесь сделать с помощью чисто веб-API-проекта? Я работаю над проектом, который кто-то еще настроил, и Authorize перенаправляет, как описано здесь, но у меня есть другой проект API, который работает нормально. Должно быть что-то, заставляющее думать, что это приложение MVC, а не приложение API, но я не могу найти то, что могло бы его испортить.
Дерек Грир

123

Брок Аллен имеет хороший пост в блоге о том, как вернуть 401 для вызовов ajax при использовании аутентификации Cookie и OWIN. http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/

Поместите это в метод ConfigureAuth в файле Startup.Auth.cs:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
  AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  LoginPath = new PathString("/Account/Login"),
  Provider = new CookieAuthenticationProvider
  {
    OnApplyRedirect = ctx =>
    {
      if (!IsAjaxRequest(ctx.Request))
      {
        ctx.Response.Redirect(ctx.RedirectUri);
      }
    }
  }
});

private static bool IsAjaxRequest(IOwinRequest request)
{
  IReadableStringCollection query = request.Query;
  if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest"))
  {
     return true;
  }
  IHeaderDictionary headers = request.Headers;
  return ((headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest"));
}

68
Вариант этого: если все ваши вызовы Web API проходят по определенному пути, например /api, вы можете использовать этот путь, чтобы определить, следует ли перенаправить. Это особенно полезно, если у вас есть клиенты, которые используют другие форматы, такие как JSON. Заменить вызов IsAjaxRequestс if (!context.Request.Path.StartsWithSegments(new PathString("/api"))).
Эдвард Брей

Опоздал на вечеринку, но этот метод - единственный, который беспокоит меня и кажется более «точным».
Стивен Коллинз

Даже поздно (r) на вечеринку, но это оказалось очень полезным ... я поражаюсь, что код, сгенерированный по умолчанию, делает это так неправильно, так расстраивающе трудно отлаживать.
Ник

Если вы ищете решение WebApi, ответ Маника является хорошей альтернативой комментарию с высоким рейтингом здесь.
Дунк

5
Используя C # 6, вот уменьшенная версия IsAjaxRequest: private static bool IsAjaxRequest(IOwinRequest request) { return request.Query?["X-Requested-With"] == "XMLHttpRequest" || request.Headers?["X-Requested-With"] == "XMLHttpRequest"; }
Peter Örneholm

85

Если вы добавляете asp.net WebApi на веб-сайт asp.net MVC, вы, вероятно, захотите ответить на некоторые запросы без разрешения. Но затем вступает в действие инфраструктура ASP.NET, и когда вы пытаетесь установить код статуса ответа HttpStatusCode.Unauthorized, вы получите 302 перенаправления на страницу входа.

Если вы используете идентификацию asp.net и аутентификацию на основе owin, вот код, который может помочь решить эту проблему:

public void ConfigureAuth(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                if (!IsApiRequest(ctx.Request))
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });

    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}


private static bool IsApiRequest(IOwinRequest request)
{
    string apiPath = VirtualPathUtility.ToAbsolute("~/api/");
    return request.Uri.LocalPath.StartsWith(apiPath);
}

1
я изменил дискриминант, чтобы проверить, принимают ли запросы text / html или application / xhtml в качестве ответа, если они не принимают, я предполагаю, что это «автоматический» клиент, такой запрос ajax
L.Trabacchin

4
Я тоже предпочитаю такой подход. Единственное добавление, которое я сделал, было преобразование LocalPath .ToLower () в случае, если они запрашивают «/ API» или что-то в этом роде.
FirstDivision

1
Большое спасибо. Это спасло мой день. :)
Амит Кумар

Кому-нибудь повезло с этим? CookieAuthenticationOptions больше не имеет свойства Provider начиная с ядра 1.1 aspnet.
Джереми

27

У меня возникла такая же ситуация, когда OWIN всегда перенаправляет ответ 401 на страницу входа из WebApi. Наш веб-API поддерживает не только вызовы ajax из Angular, но и вызовы Mobile, Win Form. Поэтому решение для проверки того, является ли запрос ajax-запросом, в нашем случае на самом деле не отсортировано.

Я выбрал другой подход - ввести новый заголовок ответа: Suppress-Redirectесли ответы приходят из webApi. Реализация находится на обработчике:

public class SuppressRedirectHandler : DelegatingHandler
{
    /// <summary>
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(task =>
        {
            var response = task.Result;
            response.Headers.Add("Suppress-Redirect", "True");
            return response;
        }, cancellationToken);
    }
}

И зарегистрируйте этот обработчик на глобальном уровне WebApi:

config.MessageHandlers.Add(new SuppressRedirectHandler());

Итак, при запуске OWIN вы можете проверить, имеет ли заголовок ответа Suppress-Redirect:

public void Configuration(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationMode = AuthenticationMode.Active,
        AuthenticationType = DefaultApplicationTypes.ApplicationCookie,
        ExpireTimeSpan = TimeSpan.FromMinutes(48),

        LoginPath = new PathString("/NewAccount/LogOn"),

        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                var response = ctx.Response;
                if (!IsApiResponse(ctx.Response))
                {
                    response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });
}

private static bool IsApiResponse(IOwinResponse response)
{
    var responseHeader = response.Headers;

    if (responseHeader == null) 
        return false;

    if (!responseHeader.ContainsKey("Suppress-Redirect"))
        return false;

    if (!bool.TryParse(responseHeader["Suppress-Redirect"], out bool suppressRedirect))
        return false;

    return suppressRedirect;
}

Спасибо ! Наши API работали на всех платформах, кроме Xamarin / Android. Будет использовать это решение
Jurion

17

В предыдущих версиях ASP.NET вам приходилось делать кучу вещей, чтобы это работало.

Хорошей новостью является то, что вы используете ASP.NET 4.5. Вы можете отключить перенаправление проверки подлинности с помощью нового свойства HttpResponse.SuppressFormsAuthenticationRedirect .

В Global.asax:

protected void Application_EndRequest(Object sender, EventArgs e)
{
        HttpApplication context = (HttpApplication)sender;
        context.Response.SuppressFormsAuthenticationRedirect = true;
}

РЕДАКТИРОВАТЬ : Вы могли бы также хотеть взглянуть на эту статью Сергея Звездина, которая имеет более изощренный способ выполнить то, что вы пытаетесь сделать.

Соответствующие фрагменты кода и повествование автора вставлены ниже. Автор оригинального кода и повествования - Сергей Звездин .

Сначала - определим, является ли текущий HTTP-запрос AJAX-запросом. Если да, мы должны отключить замену HTTP 401 на HTTP 302:

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;

        if (request.IsAjaxRequest())
            response.SuppressFormsAuthenticationRedirect = true;

        base.HandleUnauthorizedRequest(filterContext);
    }
}

Второе - давайте добавим условие :: если пользователь прошел аутентификацию, мы отправим HTTP 403; и HTTP 401 в противном случае.

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;
        var user = httpContext.User;

        if (request.IsAjaxRequest())
        {
            if (user.Identity.IsAuthenticated == false)
                response.StatusCode = (int)HttpStatusCode.Unauthorized;
            else
                response.StatusCode = (int)HttpStatusCode.Forbidden;

            response.SuppressFormsAuthenticationRedirect = true;
            response.End();
        }

        base.HandleUnauthorizedRequest(filterContext);
    }
}

Отлично сработано. Теперь мы должны заменить все использования стандартного AuthorizeAttribute этим новым фильтром. Это может быть неприменимо для тех парней, которые занимаются программированием. Но я не знаю другого пути. Если у вас есть, давайте перейдем к комментариям, пожалуйста.

Последнее, что нам нужно сделать - добавить обработку HTTP 401/403 на стороне клиента. Мы можем использовать ajaxError в jQuery, чтобы избежать дублирования кода:

$(document).ajaxError(function (e, xhr) {
    if (xhr.status == 401)
        window.location = "/Account/Login";
    else if (xhr.status == 403)
        alert("You have no enough permissions to request this resource.");
});

Результат -

  • Если пользователь не аутентифицирован, то после любого AJAX-вызова он будет перенаправлен на страницу входа.
  • Если пользователь прошел проверку подлинности, но не имеет достаточных разрешений, он увидит удобное сообщение erorr.
  • Если пользователь авторизован и имеет достаточные права, ошибок нет, и HTTP-запрос будет выполняться в обычном режиме.

Я использую новую систему идентификации для аутентификации через MVC. Разве этот параметр не помешает работе входа в систему mvc, а также вызовам webapi?
Тим

5
Когда я проверил этот пример, оказалось, что используемый атрибут Authorize является версией MVC, а не версией WebApi. однако версия webapi не имеет опций для подавления аутентификации форм.
Тим

мой запрос не имеет метода IsAjaxRequest.
Тим

1
Тим, посмотрите на это для IsAjaxRequest: brockallen.com/2013/10/27/… Если вы используете AngularJ без редактирования заголовков, у вас не будет «XMLHttpRequest», и вы либо добавите его, либо проверите что-то еще.
Тим

10

Если вы запускаете свой Web APIизнутри вашего MVCпроекта, вам нужно создать кастом, который AuthorizeAttributeбудет применяться к вашим APIметодам. Внутри IsAuthorized overrideвам нужно захватить ток, HttpContextчтобы предотвратить перенаправление, вот так:

    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        if (string.IsNullOrWhiteSpace(Thread.CurrentPrincipal.Identity.Name))
        {
            var response = HttpContext.Current.Response;
            response.SuppressFormsAuthenticationRedirect = true;
            response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
            response.End();
        }

        return base.IsAuthorized(actionContext);
    }

8

При использовании интеграции с Azure Active Directory сам подход с использованием CookieAuthenticationпромежуточного программного обеспечения не работал для меня. Я должен был сделать следующее:

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ...
        Notifications = new OpenIdConnectAuthenticationNotifications
        {   
            ...         
            RedirectToIdentityProvider = async context =>
            {
                if (!context.Request.Accept.Contains("html"))
                {
                    context.HandleResponse();
                }
            },
            ...
        }
    });

Если запрос поступает от самого браузера (а не от вызова AJAX, например), тогда заголовок Accept где-то будет содержать строку html. Только когда клиент примет HTML, я буду считать перенаправление чем-то полезным.

Мое клиентское приложение может обрабатывать 401, информируя пользователя о том, что приложение больше не имеет доступа и нуждается в перезагрузке для повторного входа в систему.


Это очень похоже на решение, предложенное для соответствующего вопроса: stackoverflow.com/questions/34997674/…
Гийом

6

У меня также было приложение MVC5 (System.Web) с WebApi (с использованием OWIN), и я просто хотел предотвратить изменение 401 ответа WebApi на 302 ответа.

Для меня работало создание настраиваемой версии WebApi AuthorizeAttribute следующим образом:

public class MyAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        base.HandleUnauthorizedRequest(actionContext);
        HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true;
    }
}

И использовать его вместо стандартного WebApi AuthorizeAttribute. Я использовал стандартный MVC AuthorizeAttribute, чтобы сохранить поведение MVC без изменений.


Работает, но теперь у меня проблема в том, что клиент получает статус -1 вместо 401
Себастьян Рохас

@ SebastiánRojas Я не уверен, что вызвало бы это - установка SuppressFormsAuthenticationRedirect флага заставила его просто вернуть существующий 401 для меня.
Jono Job

3

Просто установите следующий пакет NeGet

Установочный пакет Microsoft.AspNet.WebApi.Owin

Запишите следующий код в файл WebApiConfig.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Web API configuration and services
        //Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
    }
}

Все , что мне нужно сделать , это добавить этот фильтр , и его работы в config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));противном случае User.Identity.IsAuthenticatedвсегдаfalse
Рикардо Saracino

1

если вы хотите поймать Content-Type == application / json, вы можете использовать этот код:

private static bool IsAjaxRequest(IOwinRequest request)
    {
        IReadableStringCollection queryXML = request.Query;
        if ((queryXML != null) && (queryXML["X-Requested-With"] == "XMLHttpRequest"))
        {
            return true;
        }

        IReadableStringCollection queryJSON = request.Query;
        if ((queryJSON != null) && (queryJSON["Content-Type"] == "application/json"))
        {
            return true;
        }

        IHeaderDictionary headersXML = request.Headers;
        var isAjax = ((headersXML != null) && (headersXML["X-Requested-With"] == "XMLHttpRequest"));

        IHeaderDictionary headers = request.Headers;
        var isJson = ((headers != null) && (headers["Content-Type"] == "application/json"));

        return isAjax || isJson;

    }

С уважением!!


1

Мне было трудно получить и код состояния, и текстовый ответ, работающий в методах OnAuthorization / HandleUnauthorizedRequest. Это оказалось лучшим решением для меня:

    actionContext.Response = new HttpResponseMessage()
    {
        StatusCode = HttpStatusCode.Forbidden,
        Content = new StringContent(unauthorizedMessage)
    };

1

После долгих хлопот, пытаясь избежать перенаправлений на страницу входа, я понял, что это на самом деле вполне подходит для атрибута Authorize. Это говорит иди и получить авторизацию. Вместо этого для вызовов Api, которые не авторизованы, я просто хотел не разглашать какую-либо информацию хакерам. Эту цель было проще достичь напрямую, добавив новый атрибут, полученный из Authorize, который вместо этого скрывает содержимое как ошибку 404:

public class HideFromAnonymousUsersAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
         actionContext.Response = ActionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "Access Restricted");
    }
}

1

Смешивая MVC и WebAPI, если запрос не авторизован, он будет перенаправлен на страницу входа даже в запросе WebAPI. Для этого мы можем добавить код ниже, чтобы отправить ответ на мобильное приложение

protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
    var httpContext = HttpContext.Current;
    if (httpContext == null)
    {
        base.HandleUnauthorizedRequest(actionContext);
        return;
    }

    actionContext.Response = httpContext.User.Identity.IsAuthenticated == false ?
        actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Unauthorized, "Unauthorized") :
       actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Forbidden, "Forbidden");

    httpContext.Response.SuppressFormsAuthenticationRedirect = true;
    httpContext.Response.End();
}

0

Спасибо, парни!

В моем случае я объединил ответы cuongle & Shiva и получил что-то вроде этого:

В обработчике OnException () контроллера для исключений API:

filterContext.ExceptionHandled = true;
//...
var response = filterContext.HttpContext.Response;
response.Headers.Add("Suppress-Redirect", "true");
response.SuppressFormsAuthenticationRedirect = true;

В конфигурационном коде запуска приложения:

app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider {
            OnValidateIdentity = ctx => {
                return validateFn.Invoke(ctx);
            },
            OnApplyRedirect = ctx =>
            {
                bool enableRedir = true;
                if (ctx.Response != null)
                {
                    string respType = ctx.Response.ContentType;
                    string suppress = ctx.Response.Headers["Suppress-Redirect"];
                    if (respType != null)
                    {
                        Regex rx = new Regex("^application\\/json(;(.*))?$",
                            RegexOptions.IgnoreCase);
                        if (rx.IsMatch(respType))
                        {
                            enableRedir = false;
                        }  
                    }
                    if ((!String.IsNullOrEmpty(suppress)) && (Boolean.Parse(suppress)))
                    {
                        enableRedir = false;
                    }
                }
                if (enableRedir)
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });

-1

В MVC 5 с Dot Net Framework 4.5.2 мы получаем «application / json, обычный текст ..» под заголовком «Accept». Было бы хорошо использовать, как показано ниже:

isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.