ASP.NET MVC: imposta IIdentity o IPrincipal personalizzati


650

Devo fare qualcosa di abbastanza semplice: nella mia applicazione ASP.NET MVC, voglio impostare un IIdentity / IPrincipal personalizzato. Qualunque sia più facile / più adatto. Voglio estendere il valore predefinito in modo da poter chiamare qualcosa di simile User.Identity.Ide User.Identity.Role. Niente di speciale, solo alcune proprietà extra.

Ho letto tonnellate di articoli e domande ma mi sento come se stessi rendendo più difficile di quanto non sia in realtà. Ho pensato che sarebbe stato facile. Se un utente accede, voglio impostare una IIdentity personalizzata. Quindi ho pensato che implementerò Application_PostAuthenticateRequestnel mio global.asax. Tuttavia, questo viene chiamato su ogni richiesta e non voglio fare una chiamata al database su ogni richiesta che richiederebbe tutti i dati dal database e inserisca un oggetto IPrincipal personalizzato. Anche questo sembra molto inutile, lento e nel posto sbagliato (facendo chiamate al database lì) ma potrei sbagliarmi. O da dove provengono questi dati?

Quindi ho pensato, ogni volta che un utente accede, posso aggiungere alcune variabili necessarie nella mia sessione, che aggiungo alla IIdentity personalizzata nel Application_PostAuthenticateRequestgestore eventi. Tuttavia, il mio Context.Sessionè nulllì, quindi anche questa non è la strada da percorrere.

Ci sto lavorando da un giorno ormai e sento che mi manca qualcosa. Questo non dovrebbe essere troppo difficile da fare, giusto? Sono anche un po 'confuso da tutte le cose (semi) correlate che ne derivano. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... io sono l'unico che trova tutto questo molto confuso?

Se qualcuno potesse dirmi una soluzione semplice, elegante ed efficiente per archiviare alcuni dati extra su una IIdità senza tutto il fuzz extra .. sarebbe fantastico! So che ci sono domande simili su SO ma se la risposta di cui ho bisogno è lì dentro, devo aver trascurato.


1
Ciao Domi, è una combinazione di memorizzazione dei dati che non cambia mai (come un ID utente) o aggiornamento del cookie direttamente dopo che l'utente ha modificato i dati che devono essere riflessi nel cookie immediatamente. Se un utente lo fa, aggiorno semplicemente il cookie con i nuovi dati. Ma provo a non memorizzare dati che cambiano spesso.
Razzie,

26
questa domanda ha 36.000 visualizzazioni e molti voti positivi. è davvero un requisito così comune - e in caso affermativo non esiste un modo migliore di tutte queste "cose ​​personalizzate"?
Simon_Weaver,

2
@Simon_Weaver È noto ASP.NET Identity, che supporta più facilmente informazioni personalizzate nel cookie crittografato.
Giovanni,

1
Sono d'accordo con te, c'è da molte informazioni come hai postato: MemberShip..., Principal, Identity. ASP.NET dovrebbe semplificare, semplificare e al massimo due approcci per gestire l'autenticazione.
banda larga

1
@Simon_Weaver Ciò dimostra chiaramente che esiste una richiesta di sistema di identità IMHO più semplice, più flessibile e più flessibile.
Niico,

Risposte:


838

Ecco come lo faccio.

Ho deciso di utilizzare IPrincipal anziché IIdentity perché significa che non devo implementare sia IIdentity sia IPrincipal.

  1. Crea l'interfaccia

    interface ICustomPrincipal : IPrincipal
    {
        int Id { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }
    
  2. CustomPrincipal

    public class CustomPrincipal : ICustomPrincipal
    {
        public IIdentity Identity { get; private set; }
        public bool IsInRole(string role) { return false; }
    
        public CustomPrincipal(string email)
        {
            this.Identity = new GenericIdentity(email);
        }
    
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
  3. CustomPrincipalSerializeModel - per serializzare le informazioni personalizzate nel campo userdata nell'oggetto FormsAuthenticationTicket.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
  4. Metodo di accesso: impostazione di un cookie con informazioni personalizzate

    if (Membership.ValidateUser(viewModel.Email, viewModel.Password))
    {
        var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First();
    
        CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel();
        serializeModel.Id = user.Id;
        serializeModel.FirstName = user.FirstName;
        serializeModel.LastName = user.LastName;
    
        JavaScriptSerializer serializer = new JavaScriptSerializer();
    
        string userData = serializer.Serialize(serializeModel);
    
        FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                 1,
                 viewModel.Email,
                 DateTime.Now,
                 DateTime.Now.AddMinutes(15),
                 false,
                 userData);
    
        string encTicket = FormsAuthentication.Encrypt(authTicket);
        HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
        Response.Cookies.Add(faCookie);
    
        return RedirectToAction("Index", "Home");
    }
    
  5. Global.asax.cs - Lettura del cookie e sostituzione dell'oggetto HttpContext.User, ciò avviene eseguendo l'override di PostAuthenticateRequest

    protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
    
        if (authCookie != null)
        {
            FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
    
            JavaScriptSerializer serializer = new JavaScriptSerializer();
    
            CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData);
    
            CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);
            newUser.Id = serializeModel.Id;
            newUser.FirstName = serializeModel.FirstName;
            newUser.LastName = serializeModel.LastName;
    
            HttpContext.Current.User = newUser;
        }
    }
    
  6. Accesso nelle viste Rasoio

    @((User as CustomPrincipal).Id)
    @((User as CustomPrincipal).FirstName)
    @((User as CustomPrincipal).LastName)
    

e nel codice:

    (User as CustomPrincipal).Id
    (User as CustomPrincipal).FirstName
    (User as CustomPrincipal).LastName

Penso che il codice sia autoesplicativo. In caso contrario, fammi sapere.

Inoltre, per semplificare ulteriormente l'accesso, è possibile creare un controller di base e sovrascrivere l'oggetto Utente restituito (HttpContext.User):

public class BaseController : Controller
{
    protected virtual new CustomPrincipal User
    {
        get { return HttpContext.User as CustomPrincipal; }
    }
}

e quindi, per ciascun controller:

public class AccountController : BaseController
{
    // ...
}

che ti permetterà di accedere a campi personalizzati in codice come questo:

User.Id
User.FirstName
User.LastName

Ma questo non funzionerà all'interno delle viste. Per questo è necessario creare un'implementazione WebViewPage personalizzata:

public abstract class BaseViewPage : WebViewPage
{
    public virtual new CustomPrincipal User
    {
        get { return base.User as CustomPrincipal; }
    }
}

public abstract class BaseViewPage<TModel> : WebViewPage<TModel>
{
    public virtual new CustomPrincipal User
    {
        get { return base.User as CustomPrincipal; }
    }
}

Rendilo un tipo di pagina predefinito in Views / web.config:

<pages pageBaseType="Your.Namespace.BaseViewPage">
  <namespaces>
    <add namespace="System.Web.Mvc" />
    <add namespace="System.Web.Mvc.Ajax" />
    <add namespace="System.Web.Mvc.Html" />
    <add namespace="System.Web.Routing" />
  </namespaces>
</pages>

e nelle viste puoi accedervi in ​​questo modo:

@User.FirstName
@User.LastName

9
Bella implementazione; fai attenzione a RoleManagerModule che sostituisce il tuo principal personalizzato con un RolePrincipal. Questo mi ha causato molto dolore - stackoverflow.com/questions/10742259/…
David Keaveny,

9
ok ho trovato la soluzione, basta aggiungere un altro interruttore che passi "" (stringa vuota) poiché l'email e l'identità saranno anonime.
Vigilia di Pierre-Alain,

3
DateTime.Now.AddMinutes (N) ... come fare in modo che non si disconnetta l'utente dopo N minuti, è possibile persistere l'utente che ha effettuato l'accesso (ad esempio quando l'utente seleziona "Ricordami")?
1110

4
Se si utilizza il WebApiController, è necessario impostare Thread.CurrentPrincipala Application_PostAuthenticateRequestper farlo funzionare in quanto non si basa suHttpContext.Current.User
Jonathan Levison

3
@AbhinavGujjar FormsAuthentication.SignOut();funziona bene per me.
LukeP

109

Non posso parlare direttamente per ASP.NET MVC, ma per ASP.NET Web Forms, il trucco è creare FormsAuthenticationTickete crittografarlo in un cookie una volta che l'utente è stato autenticato. In questo modo, devi solo chiamare il database una volta (o AD o qualunque cosa tu stia utilizzando per eseguire la tua autenticazione) e ogni richiesta successiva verrà autenticata in base al ticket memorizzato nel cookie.

Un buon articolo su questo: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (link non funzionante)

Modificare:

Poiché il link sopra riportato è interrotto, consiglierei la soluzione di LukeP nella sua risposta sopra: https://stackoverflow.com/a/10524305 - Suggerirei anche di cambiare la risposta accettata in quella.

Modifica 2: un'alternativa al link non funzionante: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html


Proveniente da PHP, ho sempre inserito informazioni come UserID e altri pezzi necessari per garantire l'accesso limitato in Session. Memorizzarlo sul lato client mi rende nervoso, puoi commentare perché non sarà un problema?
John Zumbrum,

@JohnZ - il ticket stesso è crittografato sul server prima di essere inviato via cavo, quindi non è come se il client avesse accesso ai dati memorizzati all'interno del ticket. Si noti che gli ID di sessione sono memorizzati anche in un cookie, quindi non è poi così diverso.
John Rasch,

3
Se sei qui dovresti guardare la soluzione di
LukeP

2
Mi sono sempre preoccupato del potenziale per il superamento della dimensione massima dei cookie ( stackoverflow.com/questions/8706924/… ) con questo approccio. Tendo a utilizzare il Cachecome Sessionsostituto per mantenere i dati sul server. Qualcuno può dirmi se questo è un approccio imperfetto?
Red Taz,

2
Bel approccio. Un potenziale problema con questo è se il tuo oggetto utente ha più di alcune proprietà (e specialmente se ci sono oggetti nidificati), la creazione del cookie fallirà silenziosamente una volta che il valore crittografato è su 4KB (molto più facile da colpire, potresti pensare). Se memorizzi solo i dati chiave va bene, ma dovresti premere DB ancora per il resto. Un'altra considerazione è "l'aggiornamento" dei dati dei cookie quando l'oggetto utente presenta modifiche alla firma o alla logica.
Geoffrey Hudik,

63

Ecco un esempio per completare il lavoro. bool isValid viene impostato guardando alcuni archivi di dati (diciamo il tuo database di utenti). UserID è solo un ID che sto mantenendo. È possibile aggiungere informazioni aggiuntive come l'indirizzo e-mail ai dati dell'utente.

protected void btnLogin_Click(object sender, EventArgs e)
{         
    //Hard Coded for the moment
    bool isValid=true;
    if (isValid) 
    {
         string userData = String.Empty;
         userData = userData + "UserID=" + userID;
         FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(30), true, userData);
         string encTicket = FormsAuthentication.Encrypt(ticket);
         HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
         Response.Cookies.Add(faCookie);
         //And send the user where they were heading
         string redirectUrl = FormsAuthentication.GetRedirectUrl(username, false);
         Response.Redirect(redirectUrl);
     }
}

nell'asax golbal aggiungi il seguente codice per recuperare le tue informazioni

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    HttpCookie authCookie = Request.Cookies[
             FormsAuthentication.FormsCookieName];
    if(authCookie != null)
    {
        //Extract the forms authentication cookie
        FormsAuthenticationTicket authTicket = 
               FormsAuthentication.Decrypt(authCookie.Value);
        // Create an Identity object
        //CustomIdentity implements System.Web.Security.IIdentity
        CustomIdentity id = GetUserIdentity(authTicket.Name);
        //CustomPrincipal implements System.Web.Security.IPrincipal
        CustomPrincipal newUser = new CustomPrincipal();
        Context.User = newUser;
    }
}

Quando utilizzerai le informazioni in un secondo momento, puoi accedere all'entità personalizzata come segue.

(CustomPrincipal)this.User
or 
(CustomPrincipal)this.Context.User

questo ti permetterà di accedere alle informazioni personalizzate dell'utente.


2
Cordiali saluti - è Request.Cookies [] (plurale)
Dan Esparza,

10
Non dimenticare di impostare Thread.CurrentPrincipal e Context.User su CustomPrincipal.
Russ Cam

6
Da dove viene GetUserIdentity ()?
Ryan,

Come ho già detto nel commento, fornisce un'implementazione di System.Web.Security.IIdentity. Google su quell'interfaccia
Sriwantha Attanayake,

16

MVC ti fornisce il metodo OnAuthorize che pende dalle tue classi di controller. In alternativa, è possibile utilizzare un filtro azioni personalizzato per eseguire l'autorizzazione. MVC lo rende abbastanza facile da fare. Ho pubblicato un post sul blog qui. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0


Ma la sessione può essere persa e l'utente può ancora autenticarsi. No ?
Dragouf,

@brady gaster, ho letto il tuo post sul blog (grazie!), perché qualcuno dovrebbe usare l'override "OnAuthorize ()" come menzionato nel tuo post sulla voce global.asax "... AuthenticateRequest (..)" menzionato dall'altro risposte? Uno è preferito rispetto all'altro nell'impostazione dell'utente principale?
RayLoveless,

10

Ecco una soluzione se è necessario collegare alcuni metodi a @User per l'utilizzo nelle viste. Nessuna soluzione per una seria personalizzazione dell'appartenenza, ma se la domanda originale fosse necessaria solo per le visualizzazioni, forse questo sarebbe sufficiente. Quanto segue è stato usato per controllare una variabile restituita da un filtro autorizza, usato per verificare se alcuni link dovevano essere presentati o meno (non per alcun tipo di logica di autorizzazione o concessione dell'accesso).

using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Security.Principal;

    namespace SomeSite.Web.Helpers
    {
        public static class UserHelpers
        {
            public static bool IsEditor(this IPrincipal user)
            {
                return null; //Do some stuff
            }
        }
    }

Quindi aggiungi un riferimento nelle aree web.config e chiamalo come sotto nella vista.

@User.IsEditor()

1
Nella tua soluzione, abbiamo di nuovo bisogno di effettuare chiamate al database ogni volta. Perché l'oggetto utente non ha proprietà personalizzate. Ha solo nome e IsAuthanticated
oneNiceFriend il

Dipende interamente dalla tua implementazione e dal comportamento desiderato. Il mio esempio contiene 0 righe di database, o ruolo, logica. Se si utilizza IsInRole, a sua volta potrebbe essere memorizzato nella cache dei cookie, credo. Oppure si implementa la propria logica di memorizzazione nella cache.
Base

3

Basato sulla risposta di LukeP e aggiungi alcuni metodi per configurare timeoute requireSSLcollaborare Web.config.

I collegamenti dei riferimenti

Codici modificati di LukeP

1, impostato in timeoutbase a Web.Config. Il FormsAuthentication.Timeout otterrà il valore di timeout, che è definito nel web.config. Ho racchiuso i seguenti in una funzione, che restituisce un ticketritorno.

int version = 1;
DateTime now = DateTime.Now;

// respect to the `timeout` in Web.config.
TimeSpan timeout = FormsAuthentication.Timeout;
DateTime expire = now.Add(timeout);
bool isPersist = false;

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
     version,          
     name,
     now,
     expire,
     isPersist,
     userData);

2, Configura il cookie per essere sicuro o meno, in base alla RequireSSLconfigurazione.

HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
// respect to `RequreSSL` in `Web.Config`
bool bSSL = FormsAuthentication.RequireSSL;
faCookie.Secure = bSSL;

3

Va bene, quindi sono un serio cripta qui trascinando questa domanda molto vecchia, ma c'è un approccio molto più semplice a questo, che è stato toccato da @Baserz sopra. E questo è usare una combinazione di metodi di estensione C # e memorizzazione nella cache (NON usare la sessione).

In effetti, Microsoft ha già fornito una serie di tali estensioni nello Microsoft.AspNet.Identity.IdentityExtensionsspazio dei nomi. Ad esempio, GetUserId()è un metodo di estensione che restituisce l'ID utente. C'è anche GetUserName()e FindFirstValue(), che restituisce richieste basate su IPrincipal.

Quindi è necessario includere solo lo spazio dei nomi e quindi chiamare User.Identity.GetUserName()per ottenere il nome degli utenti come configurato da ASP.NET Identity.

Non sono sicuro che questo sia memorizzato nella cache, poiché la vecchia identità ASP.NET non è di origine aperta e non mi sono preoccupata di decodificarla. Tuttavia, in caso contrario, puoi scrivere il tuo metodo di estensione, che memorizzerà questo risultato nella cache per un determinato periodo di tempo.


Perché "non usare la sessione"?
Alex,

@jitbit - perché la sessione non è affidabile e non è sicura. Per lo stesso motivo non dovresti mai usare la sessione per motivi di sicurezza.
Erik Funkenbusch,

"Inaffidabile" può essere risolto ripopolando la sessione (se vuota). "Non sicuro": esistono modi per proteggersi dal dirottamento della sessione (utilizzando solo HTTPS + altri modi). Ma in realtà sono d'accordo con te. Dove lo vorresti memorizzare nella cache? Informazioni come IsUserAdministratoro UserEmailecc.? Stai pensando HttpRuntime.Cache?
Alex,

@jitbit: questa è un'opzione o un'altra soluzione di cache se ce l'hai. Assicurarsi di far scadere la voce della cache dopo un periodo di tempo. Non sicuro si applica anche al sistema locale, poiché è possibile modificare manualmente i cookie e indovinare gli ID di sessione. L'uomo nel mezzo non è l'unica preoccupazione.
Erik Funkenbusch,

2

Come aggiunta al codice LukeP per gli utenti di Web Forms (non MVC) se si desidera semplificare l'accesso al codice dietro le pagine, è sufficiente aggiungere il codice sottostante a una pagina di base e derivare la pagina di base in tutte le pagine:

Public Overridable Shadows ReadOnly Property User() As CustomPrincipal
    Get
        Return DirectCast(MyBase.User, CustomPrincipal)
    End Get
End Property

Quindi nel tuo codice dietro puoi semplicemente accedere a:

User.FirstName or User.LastName

Quello che mi manca in uno scenario Web Form, è come ottenere lo stesso comportamento nel codice non legato alla pagina, ad esempio in httpmodules dovrei sempre aggiungere un cast in ogni classe o c'è un modo più intelligente per ottenerlo?

Grazie per le vostre risposte e grazie a LukeP da quando ho usato i tuoi esempi come base per il mio utente personalizzata (che ha ora User.Roles, User.Tasks, User.HasPath(int), User.Settings.Timeoute tante altre belle cose)


0

Ho provato la soluzione suggerita da LukeP e ho scoperto che non supporta l'attributo Autorizza. Quindi l'ho modificato un po '.

public class UserExBusinessInfo
{
    public int BusinessID { get; set; }
    public string Name { get; set; }
}

public class UserExInfo
{
    public IEnumerable<UserExBusinessInfo> BusinessInfo { get; set; }
    public int? CurrentBusinessID { get; set; }
}

public class PrincipalEx : ClaimsPrincipal
{
    private readonly UserExInfo userExInfo;
    public UserExInfo UserExInfo => userExInfo;

    public PrincipalEx(IPrincipal baseModel, UserExInfo userExInfo)
        : base(baseModel)
    {
        this.userExInfo = userExInfo;
    }
}

public class PrincipalExSerializeModel
{
    public UserExInfo UserExInfo { get; set; }
}

public static class IPrincipalHelpers
{
    public static UserExInfo ExInfo(this IPrincipal @this) => (@this as PrincipalEx)?.UserExInfo;
}


    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginModel details, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            AppUser user = await UserManager.FindAsync(details.Name, details.Password);

            if (user == null)
            {
                ModelState.AddModelError("", "Invalid name or password.");
            }
            else
            {
                ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
                AuthManager.SignOut();
                AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);

                user.LastLoginDate = DateTime.UtcNow;
                await UserManager.UpdateAsync(user);

                PrincipalExSerializeModel serializeModel = new PrincipalExSerializeModel();
                serializeModel.UserExInfo = new UserExInfo()
                {
                    BusinessInfo = await
                        db.Businesses
                        .Where(b => user.Id.Equals(b.AspNetUserID))
                        .Select(b => new UserExBusinessInfo { BusinessID = b.BusinessID, Name = b.Name })
                        .ToListAsync()
                };

                JavaScriptSerializer serializer = new JavaScriptSerializer();

                string userData = serializer.Serialize(serializeModel);

                FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                         1,
                         details.Name,
                         DateTime.Now,
                         DateTime.Now.AddMinutes(15),
                         false,
                         userData);

                string encTicket = FormsAuthentication.Encrypt(authTicket);
                HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
                Response.Cookies.Add(faCookie);

                return RedirectToLocal(returnUrl);
            }
        }
        return View(details);
    }

E infine in Global.asax.cs

    protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];

        if (authCookie != null)
        {
            FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            PrincipalExSerializeModel serializeModel = serializer.Deserialize<PrincipalExSerializeModel>(authTicket.UserData);
            PrincipalEx newUser = new PrincipalEx(HttpContext.Current.User, serializeModel.UserExInfo);
            HttpContext.Current.User = newUser;
        }
    }

Ora posso accedere ai dati in viste e controller semplicemente chiamando

User.ExInfo()

Per disconnettersi ho appena chiamato

AuthManager.SignOut();

dove si trova AuthManager

HttpContext.GetOwinContext().Authentication
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.