Неавторизованный 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;