Perché AuthorizeAttribute reindirizza alla pagina di accesso per errori di autenticazione e autorizzazione?


265

In ASP.NET MVC è possibile contrassegnare un metodo del controller con AuthorizeAttribute, in questo modo:

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

Ciò significa che, se l'utente attualmente connesso non è nel ruolo "CanDeleteTags", il metodo del controller non verrà mai chiamato.

Sfortunatamente, per gli errori, AuthorizeAttributerestituisce HttpUnauthorizedResult, che restituisce sempre il codice di stato HTTP 401. Ciò causa un reindirizzamento alla pagina di accesso.

Se l'utente non ha effettuato l'accesso, questo ha perfettamente senso. Tuttavia, se l'utente ha già effettuato l'accesso, ma non ricopre il ruolo richiesto, è poco chiaro rimandarlo alla pagina di accesso.

Sembra che AuthorizeAttributecombini autenticazione e autorizzazione.

Sembra un po 'una svista in ASP.NET MVC o mi manca qualcosa?

Ho dovuto cucinare uno DemandRoleAttributeche separasse i due. Quando l'utente non è autenticato, restituisce HTTP 401, inviandolo alla pagina di accesso. Quando l'utente ha effettuato l'accesso, ma non è nel ruolo richiesto, crea un file NotAuthorizedResult. Attualmente questo reindirizza a una pagina di errore.

Sicuramente non dovevo farlo?


10
Ottima domanda e sono d'accordo, dovrebbe lanciare uno stato HTTP non autorizzato.
Pure.Krome

3
Mi piace la tua soluzione, Roger. Anche se non lo fai.
Jon Davis

La mia pagina di accesso ha un segno di spunta per reindirizzare semplicemente l'utente a ReturnUrl, se è già autenticato. Quindi sono riuscito a creare un ciclo infinito di reindirizzamenti 302: D woot.
juhan_h

1
Dai un'occhiata a questo .
Jogi

Roger, buon articolo sulla tua soluzione - red-gate.com/simple-talk/dotnet/asp-net/… Sembra che la tua soluzione sia l'unico modo per farlo in modo pulito
Craig

Risposte:


305

Quando è stato sviluppato per la prima volta, System.Web.Mvc.AuthorizeAttribute stava facendo la cosa giusta: le versioni precedenti della specifica HTTP utilizzavano il codice di stato 401 sia per "non autorizzato" che per "non autenticato".

Dalla specifica originale:

Se la richiesta includeva già le credenziali di autorizzazione, la risposta 401 indica che l'autorizzazione è stata rifiutata per tali credenziali.

In effetti, puoi vedere la confusione proprio lì: usa la parola "autorizzazione" quando significa "autenticazione". Nella pratica quotidiana, tuttavia, ha più senso restituire un 403 Forbidden quando l'utente è autenticato ma non autorizzato. È improbabile che l'utente disponga di un secondo set di credenziali che gli dia accesso: un'esperienza utente negativa tutto intorno.

Considera la maggior parte dei sistemi operativi: quando tenti di leggere un file a cui non sei autorizzato ad accedere, non ti viene mostrata una schermata di accesso!

Per fortuna, le specifiche HTTP sono state aggiornate (giugno 2014) per rimuovere l'ambiguità.

Da "Hyper Text Transport Protocol (HTTP / 1.1): autenticazione" (RFC 7235):

Il codice di stato 401 (Non autorizzato) indica che la richiesta non è stata applicata perché manca di credenziali di autenticazione valide per la risorsa di destinazione.

Da "Hypertext Transfer Protocol (HTTP / 1.1): Semantics and Content" (RFC 7231):

Il codice di stato 403 (Forbidden) indica che il server ha compreso la richiesta ma rifiuta di autorizzarla.

È interessante notare che al momento del rilascio di ASP.NET MVC 1 il comportamento di AuthorizeAttribute era corretto. Ora, il comportamento non è corretto: la specifica HTTP / 1.1 è stata corretta.

Piuttosto che tentare di modificare i reindirizzamenti della pagina di accesso di ASP.NET, è più semplice risolvere il problema alla fonte. Puoi creare un nuovo attributo con lo stesso nome ( AuthorizeAttribute) nello spazio dei nomi predefinito del tuo sito web (questo è molto importante), quindi il compilatore lo raccoglierà automaticamente al posto di quello standard di MVC. Ovviamente, potresti sempre dare all'attributo un nuovo nome se preferisci adottare questo approccio.

[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 Ottimo approccio. Un piccolo suggerimento: invece di controllare filterContext.HttpContext.User.Identity.IsAuthenticated, puoi semplicemente controllare filterContext.HttpContext.Request.IsAuthenticated, che viene fornito con controlli nulli incorporati. Vedi stackoverflow.com/questions/1379566/…
Daniel Liuzzi

> Puoi creare un nuovo attributo con lo stesso nome (AuthorizeAttribute) nello spazio dei nomi predefinito del tuo sito web, quindi il compilatore lo raccoglierà automaticamente al posto di quello standard di MVC. Ciò si traduce in un errore: Impossibile trovare il tipo o lo spazio dei nomi "Authorize" (manca una direttiva o un riferimento all'assembly?) Entrambi utilizzano System.Web.Mvc; e lo spazio dei nomi per la mia classe AuthorizeAttribute personalizzata sono referenziati nel controller. Per risolvere questo problema ho dovuto usare [MyNamepace.Authorize]
stormwild

2
@DePeter le specifiche non dicono mai nulla su un reindirizzamento, quindi perché un reindirizzamento è una soluzione migliore? Questo da solo uccide le richieste ajax senza un hack per risolverlo.
Adam Tuliper - MSFT

1
Dovrebbe essere registrato su MS Connect perché è chiaramente un bug comportamentale. Grazie.
Tony Wall

BTW, perché siamo reindirizzati alla pagina di accesso? Perché non emettere semplicemente un codice 401 e la pagina di accesso direttamente all'interno della stessa richiesta?
SandRock

24

Aggiungilo alla funzione Pagina di accesso_Load:

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

Quando l'utente viene reindirizzato lì ma è già loggato, mostra la pagina non autorizzata. Se non sono registrati, cade e mostra la pagina di accesso.


18
Page_Load is a webforms mojo
Chance

2
@Chance - quindi fallo nel ActionMethod predefinito per il controller che viene chiamato dove FormsAuthencation è stato impostato per chiamare.
Pure.Krome

Questo in realtà funziona davvero bene anche se per MVC dovrebbe essere qualcosa di simile a if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");dove Unauthorized è un nome di route definito.
Moses Machua

Quindi chiedi una risorsa, vieni reindirizzato a una pagina di accesso e vieni reindirizzato di nuovo a una pagina 403? Mi sembra brutto. Non riesco nemmeno a tollerare un reindirizzamento. IMO questa cosa è comunque costruita molto male.
SandRock

3
In base alla tua soluzione, se hai già effettuato l'accesso e vai alla pagina di accesso digitando l'URL ... questo ti porterebbe alla pagina non autorizzata. che non è giusto.
Rajshekar Reddy

4

Ho sempre pensato che avesse senso. Se hai effettuato l'accesso e provi a raggiungere una pagina che richiede un ruolo che non hai, verrai reindirizzato alla schermata di accesso che ti chiede di accedere con un utente che ha il ruolo.

È possibile aggiungere una logica alla pagina di accesso che verifica se l'utente è già autenticato. Potresti aggiungere un messaggio amichevole che spiega perché sono stati di nuovo imbrogliati.


4
Ho la sensazione che la maggior parte delle persone non abbia più di un'identità per una determinata app web. Se lo fanno, sono abbastanza intelligenti da pensare "il mio ID attuale non ha mojo, accedo di nuovo come l'altro".
Roger Lipscombe,

Anche se l'altro tuo punto sulla visualizzazione di qualcosa nella pagina di accesso è positivo. Grazie.
Roger Lipscombe

4

Sfortunatamente, hai a che fare con il comportamento predefinito dell'autenticazione basata su form ASP.NET. C'è una soluzione alternativa (non l'ho provata) discussa qui:

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

(Non è specifico per MVC)

Penso che nella maggior parte dei casi la soluzione migliore sia limitare l'accesso a risorse non autorizzate prima che l'utente cerchi di arrivarci. Rimuovendo / ingrigendo il collegamento o il pulsante che potrebbe portarli a questa pagina non autorizzata.

Probabilmente sarebbe bello avere un parametro aggiuntivo sull'attributo per specificare dove reindirizzare un utente non autorizzato. Ma nel frattempo, considero AuthorizeAttribute come una rete di sicurezza.


Ho in programma di rimuovere il collegamento anche in base all'autorizzazione (ho visto una domanda qui da qualche parte), quindi codificherò un metodo di estensione HtmlHelper in seguito.
Roger Lipscombe

1
Devo ancora impedire all'utente di accedere direttamente all'URL, che è l'argomento di questo attributo. Non sono molto soddisfatto della soluzione Custom 401 (sembra un po 'globale), quindi proverò a modellare il mio NotAuthorizedResult su RedirectToRouteResult ...
Roger Lipscombe

0

Prova questo nel gestore Application_EndRequest del tuo file 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

Se stai usando aspnetcore 2.0, usa questo:

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

Nel mio caso il problema era "la specifica HTTP utilizzava il codice di stato 401 sia per" non autorizzato "che per" non autenticato "". Come ha detto ShadowChaser.

Questa soluzione funziona per me:

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" });
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.