Warum leitet AuthorizeAttribute bei Authentifizierungs- und Autorisierungsfehlern zur Anmeldeseite um?


265

In ASP.NET MVC können Sie eine Controller-Methode AuthorizeAttributewie folgt markieren :

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

Dies bedeutet, dass die Controller-Methode niemals aufgerufen wird, wenn sich der aktuell angemeldete Benutzer nicht in der Rolle "CanDeleteTags" befindet.

Leider für Fehler, AuthorizeAttributeRückkehr HttpUnauthorizedResult, die immer HTTP - Statuscode zurückgibt 401. Dies bewirkt eine Umleitung auf die Login - Seite.

Wenn der Benutzer nicht angemeldet ist, ist dies absolut sinnvoll. Wenn der Benutzer jedoch bereits angemeldet ist, aber nicht die erforderliche Rolle innehat, ist es verwirrend, ihn an die Anmeldeseite zurückzusenden.

Es scheint, dass AuthorizeAttributeAuthentifizierung und Autorisierung miteinander verschmelzen.

Dies scheint in ASP.NET MVC ein kleines Versehen zu sein, oder fehlt mir etwas?

Ich musste eine kochen DemandRoleAttribute, die die beiden trennt. Wenn der Benutzer nicht authentifiziert ist, gibt er HTTP 401 zurück und sendet sie an die Anmeldeseite. Wenn der Benutzer angemeldet ist, aber nicht in der erforderlichen Rolle ist, wird NotAuthorizedResultstattdessen eine erstellt . Derzeit wird auf eine Fehlerseite weitergeleitet.

Sicher musste ich das nicht tun?


10
Ausgezeichnete Frage und ich stimme zu, es sollte einen HTTP-Status "Nicht autorisiert" auslösen.
Pure.Krome

3
Ich mag deine Lösung, Roger. Auch wenn du es nicht tust.
Jon Davis

Auf meiner Anmeldeseite wird überprüft, ob der Benutzer einfach zur ReturnUrl weitergeleitet werden soll, wenn er bereits authentifiziert ist. Also habe ich es geschafft, eine Endlosschleife von 302 Weiterleitungen zu erstellen: D woot.
juhan_h

1
Überprüfen Sie dies .
Jogi

Roger, guter Artikel über Ihre Lösung - red-gate.com/simple-talk/dotnet/asp-net/… Es scheint, dass Ihre Lösung der einzige Weg ist, dies sauber zu machen
Craig

Antworten:


305

Bei der ersten Entwicklung hat System.Web.Mvc.AuthorizeAttribute das Richtige getan - ältere Revisionen der HTTP-Spezifikation verwendeten den Statuscode 401 sowohl für "nicht autorisiert" als auch für "nicht authentifiziert".

Aus der ursprünglichen Spezifikation:

Wenn die Anforderung bereits Berechtigungsnachweise enthielt, zeigt die Antwort 401 an, dass die Berechtigung für diese Anmeldeinformationen verweigert wurde.

In der Tat können Sie die Verwirrung genau dort sehen - es verwendet das Wort "Autorisierung", wenn es "Authentifizierung" bedeutet. In der täglichen Praxis ist es jedoch sinnvoller, einen 403 Forbidden zurückzugeben, wenn der Benutzer authentifiziert, aber nicht autorisiert ist. Es ist unwahrscheinlich, dass der Benutzer über einen zweiten Satz von Anmeldeinformationen verfügt, die ihm Zugriff gewähren - eine rundum schlechte Benutzererfahrung.

Betrachten Sie die meisten Betriebssysteme. Wenn Sie versuchen, eine Datei zu lesen, auf die Sie nicht zugreifen dürfen, wird kein Anmeldebildschirm angezeigt.

Zum Glück wurden die HTTP-Spezifikationen aktualisiert (Juni 2014), um die Mehrdeutigkeit zu beseitigen.

Aus "Hyper Text Transport Protocol (HTTP / 1.1): Authentifizierung" (RFC 7235):

Der Statuscode 401 (nicht autorisiert) gibt an, dass die Anforderung nicht angewendet wurde, da keine gültigen Authentifizierungsdaten für die Zielressource vorhanden sind.

Aus "Hypertext Transfer Protocol (HTTP / 1.1): Semantik und Inhalt" (RFC 7231):

Der Statuscode 403 (Verboten) zeigt an, dass der Server die Anforderung verstanden hat, sich jedoch weigert, sie zu autorisieren.

Interessanterweise war das Verhalten von AuthorizeAttribute zum Zeitpunkt der Veröffentlichung von ASP.NET MVC 1 korrekt. Jetzt ist das Verhalten falsch - die HTTP / 1.1-Spezifikation wurde behoben.

Anstatt zu versuchen, die Weiterleitungen der Anmeldeseite von ASP.NET zu ändern, ist es einfacher, das Problem nur an der Quelle zu beheben. Sie können ein neues Attribut mit demselben Namen ( AuthorizeAttribute) im Standard-Namespace Ihrer Website erstellen (dies ist sehr wichtig). Der Compiler übernimmt es dann automatisch anstelle des Standard- Attributs von MVC. Natürlich können Sie dem Attribut jederzeit einen neuen Namen geben, wenn Sie diesen Ansatz bevorzugen.

[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 Sehr guter Ansatz. Ein kleiner Vorschlag: Anstatt zu prüfen filterContext.HttpContext.User.Identity.IsAuthenticated, können Sie einfach prüfen filterContext.HttpContext.Request.IsAuthenticated, was mit eingebauten Nullprüfungen einhergeht
Daniel Liuzzi

> Sie können ein neues Attribut mit demselben Namen (AuthorizeAttribute) im Standard-Namespace Ihrer Website erstellen. Der Compiler übernimmt es dann automatisch anstelle des Standardattributs von MVC. Dies führt zu einem Fehler: Der Typ oder Namespace 'Authorize' wurde nicht gefunden (fehlt Ihnen eine Direktive oder eine Assemblyreferenz?) Beide verwenden System.Web.Mvc; und der Namespace für meine benutzerdefinierte AuthorizeAttribute-Klasse werden im Controller referenziert. Um dies zu lösen, musste ich [MyNamepace.Authorize]
Stormwild

2
@DePeter Die Spezifikation sagt nie etwas über eine Weiterleitung aus. Warum ist eine Weiterleitung eine bessere Lösung? Dies allein beendet Ajax-Anfragen, ohne dass ein Hack vorhanden ist, um das Problem zu lösen.
Adam Tuliper - MSFT

1
Das sollte bei MS Connect protokolliert werden, da es sich eindeutig um einen Verhaltensfehler handelt. Vielen Dank.
Tony Wall

BTW, warum sind wir umgeleitet zur Login - Seite? Warum nicht einfach einen 401-Code und die Anmeldeseite direkt in derselben Anfrage ausgeben?
SandRock

25

Fügen Sie dies Ihrer Login Page_Load-Funktion hinzu:

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

Wenn der Benutzer dort umgeleitet wird, aber bereits angemeldet ist, wird die nicht autorisierte Seite angezeigt. Wenn sie nicht angemeldet sind, fällt sie durch und zeigt die Anmeldeseite an.


18
Page Load ist ein Webforms Mojo
Chance

2
@Chance - Führen Sie dies dann in der Standard-ActionMethod für den Controller aus, der aufgerufen wird, für den FormsAuthencation zum Aufrufen eingerichtet wurde.
Pure.Krome

Das funktioniert eigentlich sehr gut , aber für MVC sollte es so etwas wie , if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");wo Unerlaubte ist eine definierte Routennamen.
Moses Machua

Sie fragen also nach einer Ressource, werden auf eine Anmeldeseite umgeleitet und erneut auf eine 403-Seite umgeleitet ? Scheint mir schlecht. Ich kann überhaupt keine Weiterleitung tolerieren. IMO ist dieses Ding sowieso sehr schlecht gebaut.
SandRock

3
Wenn Sie sich bereits angemeldet haben und zur Anmeldeseite wechseln, indem Sie die URL eingeben, gelangen Sie zur Seite "Nicht autorisiert". das ist nicht richtig.
Rajshekar Reddy

4

Ich dachte immer, das macht Sinn. Wenn Sie angemeldet sind und versuchen, eine Seite aufzurufen, für die eine Rolle erforderlich ist, die Sie nicht haben, werden Sie zum Anmeldebildschirm weitergeleitet, in dem Sie aufgefordert werden, sich mit einem Benutzer anzumelden, der über die Rolle verfügt.

Sie können der Anmeldeseite eine Logik hinzufügen, die überprüft, ob der Benutzer bereits authentifiziert ist. Sie können eine freundliche Nachricht hinzufügen, die erklärt, warum sie dort wieder verpfuscht wurden.


4
Ich habe das Gefühl, dass die meisten Menschen nicht mehr als eine Identität für eine bestimmte Web-App haben. Wenn ja, sind sie klug genug zu denken, "meine aktuelle ID hat kein Mojo, ich melde mich wieder als die andere an".
Roger Lipscombe

Obwohl Ihr anderer Punkt, etwas auf der Anmeldeseite anzuzeigen, ein guter ist. Vielen Dank.
Roger Lipscombe

4

Leider haben Sie es mit dem Standardverhalten der ASP.NET-Formularauthentifizierung zu tun. Hier wird eine Problemumgehung (ich habe sie nicht ausprobiert) beschrieben:

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

(Es ist nicht spezifisch für MVC)

Ich denke, in den meisten Fällen besteht die beste Lösung darin, den Zugriff auf nicht autorisierte Ressourcen zu beschränken, bevor der Benutzer versucht, dorthin zu gelangen. Durch Entfernen / Ausgrauen des Links oder der Schaltfläche, die sie möglicherweise zu dieser nicht autorisierten Seite führen.

Es wäre wahrscheinlich schön, einen zusätzlichen Parameter für das Attribut zu haben, um anzugeben, wohin ein nicht autorisierter Benutzer umgeleitet werden soll. In der Zwischenzeit betrachte ich das AuthorizeAttribute als Sicherheitsnetz.


Ich habe vor, den Link auch aufgrund der Autorisierung zu entfernen (ich habe hier irgendwo eine Frage dazu gesehen), daher werde ich später eine HtmlHelper-Erweiterungsmethode codieren.
Roger Lipscombe

1
Ich muss immer noch verhindern, dass der Benutzer direkt zur URL wechselt, worum es bei diesem Attribut geht. Ich bin nicht sehr zufrieden mit der Custom 401-Lösung (scheint ein bisschen global zu sein), daher werde ich versuchen, mein NotAuthorizedResult auf RedirectToRouteResult zu modellieren ...
Roger Lipscombe

0

Versuchen Sie dies in Ihrem im Application_EndRequest-Handler Ihrer Global.ascx-Datei

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

0

Wenn Sie Aspnetcore 2.0 verwenden, verwenden Sie Folgendes:

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

In meinem Fall war das Problem "HTTP-Spezifikation verwendet Statuscode 401 sowohl für" nicht autorisiert "als auch" nicht authentifiziert "". Wie ShadowChaser sagte.

Diese Lösung funktioniert bei mir:

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" });
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.