Неавторизованный webapi вызов, возвращающий страницу входа, а не 401

Как мне настроить проект 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 код:

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 несанкционированных, так что я могу отображать разные данные для пользователей на основе того, вошли ли они в систему или нет. В идеале, если пользователь зарегистрирован, я хочу иметь доступ к свойству User Controller, чтобы я мог возвращать данные, специфичные для этого члена.

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

Ответ 1

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

Обновлено 11/26/2013

Итак, похоже, что в MVC 5 ситуация изменилась, поскольку Брок Аллен указал в

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

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

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

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

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

X-Requested-With: XMLHttpRequest

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

Ответ 2

У Brock Allen есть хорошее сообщение в блоге о том, как вернуть 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"));
}

Ответ 3

Если вы добавляете 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);
}

Ответ 4

У меня возникла такая же ситуация, когда 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;
}

Ответ 5

В предыдущих версиях 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-запрос будет выполняться, как обычно.

Ответ 6

Если вы запускаете Web API из своего MVC проекта, вам нужно создать собственный AuthorizeAttribute для применения к вашим API методам. В override IsAuthorized вам нужно получить текущий 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);
    }

Ответ 7

Используя интеграцию 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, информируя пользователя о том, что приложение больше не имеет доступа и ему необходимо перезагрузить для входа снова.

Ответ 8

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

Что сработало для меня, так это создать настраиваемую версию атрибута AuthorizationAttribute WebApi следующим образом:

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

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

Ответ 9

если вы хотите поймать 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;

    }

привет!!

Ответ 10

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

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

Ответ 11

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

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

Ответ 12

Просто установите следующий пакет 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"));
    }
}

Ответ 13

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

В моем случае я объединил ответы 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);
                }
            }
        }
    });

Ответ 14

Смешивая 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();
}

Ответ 15

В MVC 5 с Dot Net Framework 4.5.2 мы получаем "application/json, plaint text.." в заголовке "Accept" Будет удобно использовать, например, следующее:

isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;