Почему AuthorizeAttribute перенаправляет на страницу входа в систему при сбое аутентификации и авторизации?


265

В ASP.NET MVC вы можете пометить метод контроллера AuthorizeAttributeследующим образом:

[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
    // ...
}

Это означает, что, если зарегистрированный в данный момент пользователь не имеет роли «CanDeleteTags», метод контроллера никогда не будет вызываться.

К сожалению, для сбоев AuthorizeAttributeвозвращается HttpUnauthorizedResult, что всегда возвращает код состояния HTTP 401. Это вызывает перенаправление на страницу входа.

Если пользователь не вошел в систему, это имеет смысл. Тем не менее, если пользователь уже вошел в систему, но не в требуемой роли, отправлять их обратно на страницу входа в систему сложно.

Похоже, что AuthorizeAttributeобъединяет аутентификацию и авторизацию.

Это похоже на упущение в ASP.NET MVC, или я что-то упустил?

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

Конечно, я не должен был этого делать?


10
Отличный вопрос, и я согласен, он должен выдавать статус HTTP Not Authorized.
Pure.Krome

3
Мне нравится ваше решение, Роджер. Даже если вы этого не сделаете.
Джон Дэвис

Моя страница входа в систему имеет проверку, чтобы просто перенаправить пользователя на ReturnUrl, если он / она уже прошел аутентификацию. Поэтому мне удалось создать бесконечный цикл из 302 перенаправлений: D woot.
juhan_h

1
Проверьте это .
Йогин

Роджер, хорошая статья о вашем решении - red-gate.com/simple-talk/dotnet/asp-net/… Кажется, ваше решение - единственный способ сделать это чисто
Крейг

Ответы:


305

Когда он был впервые разработан, System.Web.Mvc.AuthorizeAttribute действовал правильно - более ранние версии спецификации HTTP использовали код состояния 401 как для «неавторизованного», так и «неаутентифицированного».

Из оригинальной спецификации:

Если в запрос уже включены учетные данные авторизации, то ответ 401 указывает, что в авторизации было отказано для этих учетных данных.

На самом деле, вы можете увидеть путаницу прямо здесь - она ​​использует слово «авторизация», когда оно означает «аутентификация». Однако в повседневной практике имеет смысл возвращать 403 Запрещено, когда пользователь аутентифицирован, но не авторизован. Маловероятно, что у пользователя будет второй набор учетных данных, который предоставит ему доступ - плохой пользовательский опыт со всех сторон.

Рассмотрим большинство операционных систем - когда вы пытаетесь прочитать файл, к которому у вас нет прав доступа, вам не показывается экран входа!

К счастью, спецификации HTTP были обновлены (июнь 2014 г.), чтобы устранить неоднозначность.

Из «Гипертекстового транспортного протокола (HTTP / 1.1): аутентификация» (RFC 7235):

Код состояния 401 (неавторизованный) указывает, что запрос не был применен, поскольку в нем отсутствуют действительные учетные данные аутентификации для целевого ресурса.

Из «Протокола передачи гипертекста (HTTP / 1.1): семантика и контент» (RFC 7231):

Код состояния 403 (Запрещено) указывает, что сервер понял запрос, но отказывается его авторизовать.

Интересно, что на момент выпуска ASP.NET MVC 1 поведение AuthorizeAttribute было правильным. Теперь поведение некорректно - спецификация HTTP / 1.1 была исправлена.

Вместо того, чтобы пытаться изменить перенаправления страницы входа в ASP.NET, проще просто устранить проблему у источника. Вы можете создать новый атрибут с таким же именем ( AuthorizeAttribute) в пространстве имен вашего сайта по умолчанию (это очень важно), тогда компилятор автоматически подберет его вместо стандартного MVC. Конечно, вы всегда можете дать атрибуту новое имя, если вы предпочитаете такой подход.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

52
+1 Очень хороший подход. Небольшое предложение: вместо проверки filterContext.HttpContext.User.Identity.IsAuthenticatedвы можете просто проверить filterContext.HttpContext.Request.IsAuthenticated, которая поставляется со встроенными нулевыми проверками. См. Stackoverflow.com/questions/1379566/…
Даниэль Лиуцци,

> Вы можете создать новый атрибут с тем же именем (AuthorizeAttribute) в пространстве имен вашего сайта по умолчанию, после чего компилятор автоматически подберет его вместо стандартного MVC. Это приводит к ошибке: Не удалось найти тип или пространство имен «Авторизация» (отсутствует директива или ссылка на сборку?) Оба с использованием System.Web.Mvc; и пространство имен для моего пользовательского класса AuthorizeAttribute упоминается в контроллере. Чтобы решить эту проблему, мне пришлось использовать [MyNamepace.Authorize]
stormwild

2
@DePeter спецификация никогда не говорит ничего о перенаправлении, так почему же перенаправление является лучшим решением? Это само по себе убивает запросы AJAX без взлома для его решения.
Адам Тюльпер - MSFT

1
Это должно быть зарегистрировано в MS Connect, потому что это явно поведенческая ошибка. Спасибо.
Тони Уолл

Кстати, почему мы перенаправлены на страницу входа? Почему бы просто не вывести код 401 и страницу входа непосредственно в одном запросе?
SandRock

25

Добавьте это к вашей функции Login Page_Load:

// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
    Response.Redirect("Unauthorized.aspx");

Когда пользователь перенаправлен туда, но уже вошел в систему, отображается неавторизованная страница. Если они не вошли в систему, он проваливается и показывает страницу входа.


18
Page_Load - это модо вебформ
шанс

2
@Chance - затем сделайте это в ActionMethod по умолчанию для контроллера, который вызывается там, где был настроен вызов FormsAuthencation.
Pure.Krome

Это на самом деле работает очень хорошо, хотя для MVC это должно быть что-то вроде if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");где Unauthorized - это определенное имя маршрута.
Моисей Мачуа

Итак, вы спрашиваете ресурс, вы перенаправляетесь на страницу входа в систему и снова перенаправляетесь на страницу 403? Кажется плохим для меня Я даже не могу терпеть одно перенаправление вообще. ИМО эта штука очень плохо построена в любом случае.
SandRock

3
Согласно вашему решению, если вы уже вошли в систему и перейдете на страницу входа, введя URL-адрес ... это приведет к переходу на страницу неавторизованных пользователей. что не правильно.
Раджшекар Редди

4

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

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


4
Мне кажется, что большинство людей не склонны иметь более одной идентичности для данного веб-приложения. Если они это сделают, то они достаточно умны, чтобы думать, что «у моего текущего идентификатора нет mojo, я войду снова как другой».
Роджер Липскомб

Хотя ваш другой пункт о отображении чего-либо на странице входа в систему является хорошим. Спасибо.
Роджер Липскомб

4

К сожалению, вы имеете дело с поведением по умолчанию проверки подлинности форм ASP.NET. Здесь есть обходной путь (я не пробовал):

http://www.codeproject.com/KB/aspnet/Custon401Page.aspx

(Это не специфично для MVC)

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

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


Я также планирую удалить ссылку на основе авторизации (где-то здесь я видел вопрос об этом), поэтому позже я напишу метод расширения HtmlHelper.
Роджер Липскомб

1
Мне все еще нужно запретить пользователю переходить непосредственно к URL-адресу, что и является этим атрибутом. Я не слишком доволен решением Custom 401 (кажется немного глобальным), поэтому я попытаюсь смоделировать свой NotAuthorizedResult на RedirectToRouteResult ...
Роджер Липскомб

0

Попробуйте это в вашем обработчике Application_EndRequest вашего файла Global.ascx.

if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
    HttpContext.Current.Response.ClearContent();
    Response.Redirect("~/AccessDenied.aspx");
}

0

Если вы используете aspnetcore 2.0, используйте это:

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Core
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var user = context.HttpContext.User;

            if (!user.Identity.IsAuthenticated)
            {
                context.Result = new UnauthorizedResult();
                return;
            }
        }
    }
}

0

В моем случае проблема заключалась в том, что «спецификация HTTP использовала код состояния 401 как для« неавторизованного », так и« неавторизованного »». Как сказал ShadowChaser.

Это решение работает для меня:

if (User != null &&  User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
    //Do whatever

    //In my case redirect to error page
    Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.