Почему 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);
        }
    }
}
ShadowChaser
источник
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 как на сеть безопасности.

Keltex
источник
Я также планирую удалить ссылку на основе авторизации (где-то здесь я видел вопрос об этом), поэтому позже я напишу метод расширения 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" });
}
Сезар Леон
источник