Несанкціонована веб-сторінка для входу, що повертається, а не 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, якби ви попросили дії контролера, на якому було позначено [Авторизація], і ви не ввійшли в систему.

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

ОНОВЛЕННЯ: Оскільки, здається, жодна з наведених нижче пропозицій не працює (зміни в Identity або WebAPI), ive створив грубний приклад на github, який повинен ілюструвати проблему.

Відповіді:


78

Є дві реалізації AutizeAttribute, і вам потрібно переконатися, що ви посилаєтеся на правильний для веб-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 помилок.

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

X-Requested-With: XMLHttpRequest

Якщо це не так, це проблема. Я не знайомий з кутовими, але якщо він дозволяє вставити власні значення заголовка, додайте це до своїх запитів ajax, і воно, ймовірно, почне працювати.


Я думаю, що я, використовуючи System.web.http.authorizeattribute, принаймні цей webapicontroller не використовує для system.web.mvc, і, переходячи до визначення атрибуту авторизації, надсилає мене до system.web.http
Тім

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

1
тільки що помітив цю відповідь (думаю, stackoverflow не надсилає сповіщення) Я додав приклад github, щоб проілюструвати проблему, і тепер додав ваше виправлення до кутових заголовків. Дякую. Однак це не здається правильним, що в атрибуті авторизації немає властивості, яку я можу перевірити, або оригінальна функціональність, яку ви згадали, більше не працює.
Тім

3
Використання POSTMAN і параметр заголовка X-Requested-With: XMLHttpRequest працює для мене ... дякую
chemitaxis

Отже, що робити, якщо ви маєте намір зробити це чистим проектом Web API? Я працюю над проектом, який створив хтось інший, і Авторизація переадресовує, як описано тут, але у мене є інший проект API, який працює добре. Повинно щось робити, думаючи, що це MVC, а не додаток API, але я не можу знайти те, що могло б поєднати це.
Дерек Грір

123

У 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"));
}

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 та автентифікацію, засновану на власній власності, код, який може допомогти вирішити цю проблему:

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
я змінив дискримінанта, щоб перевірити, чи приймають запити текст / html або додаток / xhtml як відповідь, якщо вони не вважають, що це "автоматизований" клієнт, який запитує, такий запит Ajax
L.Trabacchin

4
Я також віддаю перевагу такому підходу. Єдине доповнення, яке я зробив, - це перетворити LocalPath .ToLower () на випадок, коли вони вимагають "/ API" чи чогось іншого.
FirstDivision

1
Дуже дякую. Це врятувало мені день. :)
Аміт Кумар

Хтось має удачу з цим? CookieAuthenticationOptions більше не має властивості Постачальника як основної версії 1.1.
Джеремі

27

У мене виникла така ж ситуація, коли OWIN завжди перенаправляє відповідь 401 на сторінку входу з WebApi.Our Web API підтримує не лише Ajax дзвінки з Angular, але і мобільні, Win форму дзвінків. Тому рішення перевірити, чи є запит 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;
}

EDIT : Ви також можете поглянути на цю статтю Сергія Цвездіна, яка має більш вишуканий спосіб досягти того, що ви намагаєтеся зробити.

Відповідні фрагменти коду та авторські розповіді, які вставлені нижче. Оригінальний автор коду та розповіді - Сергій Цвездін .

Спочатку - визначимо, чи поточний 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 цим новим фільтром. Можливо, це не застосовується для хлопців sime, які є естетом коду. Але я не знаю іншого способу. Якщо у вас є, переходимо до коментарів, будь ласка.

Останнє, що нам потрібно зробити - додати обробку 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-дзвінка.
  • Якщо користувач має автентифікацію, але не має достатнього дозволу, він побачить дружнє повідомлення про помилку користувача.
  • Якщо користувач має автентифікацію та має достатньо дозволів, помилок немає, і HTTP-запит буде виконуватись як завжди.

Я використовую нову структуру ідентичності для auth через mvc. Чи це налаштування не завадить входу в mvc працювати так само добре, як і виклики webapi?
Тім

5
коли я перевірив цей приклад, видається, що використовується атрибут авторизації - це версія MVC, а не версія WebApi. Однак у версії webapi немає варіантів придушення форми аутентикуації форм.
Тім

у мого запиту немає методу IsAjaxRequest.
Тім

1
Тім погляне на це на IsAjaxRequest: brockallen.com/2013/10/27/… Якщо ви використовуєте AngularJs без редагування заголовків, у вас не буде "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 Package

Встановити-пакет 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
Рікардо

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

Після великої суєти, намагаючись уникнути переадресації на сторінку входу, я зрозумів, що це насправді цілком підходить для атрибута Авторизація. Це говорить: ідіть і отримайте авторизацію. Натомість для дзвінків Api, які не мають дозволу, я просто хотів не розкривати будь-яку інформацію, яка буде хакерами. Цю мету було легше досягти безпосередньо, додавши новий атрибут, похідний від Авторизація, який замість цього приховує вміст як помилку 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, plaint text .." у заголовку "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.