ASP.NET MVC: establezca IIdentity o IPrincipal personalizados

Necesito hacer algo bastante simple: en mi aplicación ASP.NET MVC, quiero configurar un IIdentity / IPrincipal personalizado. Cualquiera que sea más fácil / más adecuado. Quiero extender el valor predeterminado para poder llamar a algo como User.Identity.Id y User.Identity.Role. Nada especial, solo algunas propiedades extra.

He leído toneladas de artículos y preguntas, pero siento que lo estoy haciendo más difícil de lo que realmente es. Pensé que iba a ser fácil. Si un usuario inicia sesión, quiero establecer una identidad personalizada. Entonces pensé, implementaré Application_PostAuthenticateRequest en mi global.asax. Sin embargo, eso se llama en cada solicitud, y no quiero hacer una llamada a la base de datos en cada solicitud que solicitaría todos los datos de la base de datos y colocaría un objeto IPrincipal personalizado. Eso también parece muy innecesario, lento y en el lugar equivocado (haciendo llamadas a la base de datos allí) pero podría estar equivocado. ¿O de dónde más vendrían esos datos?

Entonces pensé, cada vez que un usuario inicia sesión, puedo agregar algunas variables necesarias en mi sesión, que agrego a la identidad personalizada en el Application_PostAuthenticateRequest controlador de eventos. Sin embargo, mi Context.Session is null allí, por lo que tampoco es el camino a seguir.

He estado trabajando en esto durante un día y siento que me estoy perdiendo algo. Esto no debería ser demasiado difícil de hacer, ¿verdad? También estoy un poco confundido por todas las cosas (semi) relacionadas que vienen con esto. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... ¿Soy el único que encuentra todo esto muy confuso?

Si alguien pudiera decirme una solución simple, elegante y eficiente para almacenar algunos datos adicionales en un IIdentity sin toda la confusión adicional ... ¡sería genial! Sé que hay preguntas similares sobre SO, pero si la respuesta que necesito está ahí, debo haberla pasado por alto.

preguntado el 30 de junio de 09 a las 12:06

Hola Domi, es una combinación de solo almacenar datos que nunca cambian (como una identificación de usuario) o actualizar la cookie directamente después de que el usuario cambia los datos que deben reflejarse en la cookie de inmediato. Si un usuario hace eso, simplemente actualizo la cookie con los nuevos datos. Pero trato de no almacenar datos que cambian con frecuencia. -

esta pregunta tiene 36k visitas y muchos votos a favor. ¿Es esto realmente un requisito tan común? Y si es así, ¿no hay una mejor manera que todas estas 'cosas personalizadas'? -

@Simon_Weaver Existe un conocimiento de identidad ASP.NET, que admite información personalizada adicional en la cookie cifrada con mayor facilidad. -

Estoy de acuerdo contigo, hay mucha información como la que publicaste: MemberShip..., Principal, Identity. ASP.NET debería hacer esto más fácil, más simple y como máximo dos enfoques para lidiar con la autenticación. -

@Simon_Weaver Esto muestra claramente que existe una demanda de un sistema de identidad más simple, más fácil y flexible en mi humilde opinión. -

9 Respuestas

Así es como lo hago.

Decidí usar IPrincipal en lugar de IIdentity porque significa que no tengo que implementar tanto IIdentity como IPrincipal.

  1. Crea la interfaz

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

    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: para serializar información personalizada en el campo de datos de usuario en el objeto FormsAuthenticationTicket.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
  4. Método de inicio de sesión: configuración de una cookie con información personalizada

    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: lectura de cookies y reemplazo del objeto HttpContext.User, esto se hace anulando 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. Acceso en vistas de Razor

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

y en código:

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

Creo que el código se explica por sí mismo. Si no es así, avíseme.

Además, para facilitar aún más el acceso, puede crear un controlador base y anular el objeto de usuario devuelto (HttpContext.User):

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

y luego, para cada controlador:

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

que le permitirá acceder a campos personalizados en un código como este:

User.Id
User.FirstName
User.LastName

Pero esto no funcionará en las vistas interiores. Para eso, necesitaría crear una implementación personalizada de WebViewPage:

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; }
    }
}

Conviértalo en un tipo de página predeterminado en 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>

y en vistas, puedes acceder a él así:

@User.FirstName
@User.LastName

contestado el 31 de mayo de 17 a las 18:05

Buena implementación; tenga cuidado con RoleManagerModule reemplazando su principal personalizado con un RolePrincipal. Eso me causó mucho dolor. stackoverflow.com/questions/10742259/… - david keaveny

ok, encontré la solución, solo agregue un interruptor else que pase "" (cadena vacía) como el correo electrónico y la identidad será anónima. - Pierre-Alain Vigeant

DateTime.Now.AddMinutes (N) ... cómo hacer esto para que no cierre la sesión del usuario después de N minutos, ¿se puede persistir el usuario que inició sesión (cuando el usuario marca 'Recordarme', por ejemplo)? - 1110

Si está utilizando WebApiController, deberá configurar Thread.CurrentPrincipal at Application_PostAuthenticateRequest para que funcione, ya que no depende de HttpContext.Current.User - jonathan levison

@AbhinavGujjar FormsAuthentication.SignOut(); funciona bien para mí. - lucasp

No puedo hablar directamente por ASP.NET MVC, pero para ASP.NET Web Forms, el truco es crear un FormsAuthenticationTicket y encriptarlo en una cookie una vez que el usuario haya sido autenticado. De esta manera, solo tiene que llamar a la base de datos una vez (o AD o lo que esté usando para realizar su autenticación), y cada solicitud posterior se autenticará en función del ticket almacenado en la cookie.

Un buen artículo sobre esto: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (Enlace roto)

Edit:

Dado que el enlace anterior está roto, recomendaría la solución de LukeP en su respuesta anterior: https://stackoverflow.com/a/10524305 - También sugeriría que la respuesta aceptada se cambie a esa.

Editar 2: Una alternativa para el enlace roto: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html

contestado el 23 de mayo de 17 a las 13:05

Viniendo de PHP, siempre he puesto la información como UserID y otras piezas necesarias para otorgar acceso restringido en Session. Guardarlo en el lado del cliente me pone nervioso, ¿puedes comentar por qué eso no será un problema? - Juan Zumbrum

@JohnZ: el ticket en sí está encriptado en el servidor antes de enviarse por cable, por lo que no es como si el cliente tuviera acceso a los datos almacenados en el ticket. Tenga en cuenta que los ID de sesión también se almacenan en una cookie, por lo que en realidad no es tan diferente. - Juan Rasch

Si está aquí, debería mirar la solución de LukeP: mynkow

Siempre me ha preocupado la posibilidad de superar el tamaño máximo de cookie (stackoverflow.com/questions/8706924/…) con este enfoque. Tiendo a usar el Cache como herramienta de edición del Session reemplazo para mantener los datos en el servidor. ¿Alguien puede decirme si este es un enfoque defectuoso? - Taz rojo

Buen enfoque. Un problema potencial con esto es si su objeto de usuario tiene más de unas pocas propiedades (y especialmente si hay objetos anidados), la creación de la cookie fallará silenciosamente una vez que el valor cifrado supere los 4 KB (mucho más fácil de alcanzar de lo que podría pensar). Si solo almacena datos clave, está bien, pero entonces tendría que presionar DB para el resto. Otra consideración es "actualizar" los datos de las cookies cuando el objeto del usuario tiene cambios de firma o de lógica. - Geoffrey Hudik

Aquí hay un ejemplo para hacer el trabajo. bool isValid se establece mirando algún almacén de datos (digamos su base de datos de usuario). UserID es solo una identificación que estoy manteniendo. Puede agregar información adicional como la dirección de correo electrónico a los datos del usuario.

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);
     }
}

en el asax golbal agregue el siguiente código para recuperar su información

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;
    }
}

Cuando vaya a utilizar la información más adelante, puede acceder a su principal personalizado de la siguiente manera.

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

esto le permitirá acceder a información de usuario personalizada.

Respondido 06 Oct 14, 20:10

FYI - es Request.Cookies [] (plural) - Dan Esparza

No olvide configurar Thread.CurrentPrincipal y Context.User en CustomPrincipal. - cámara rusa

¿De dónde viene GetUserIdentity ()? - Ryan

Como mencioné en el comentario, proporciona una implementación de System.Web.Security.IIdentity. Google sobre esa interfaz: Sriwantha Attanayake

MVC le proporciona el método OnAuthorize que cuelga de sus clases de controlador. O puede utilizar un filtro de acción personalizado para realizar la autorización. MVC lo hace bastante fácil de hacer. Publiqué una publicación de blog sobre esto aquí. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0

contestado el 08 de mayo de 14 a las 00:05

Pero la sesión se puede perder y el usuario aún se autentica. No ? - Dragouf

@brady gaster, leí la publicación de su blog (¡gracias!), ¿Por qué alguien usaría la anulación "OnAuthorize ()" como se menciona en su publicación sobre la entrada global.asax "... AuthenticateRequest (..)" mencionada por el otro respuestas? ¿Se prefiere uno sobre el otro al establecer el usuario principal? - Ray sin amor

Aquí hay una solución si necesita conectar algunos métodos a @User para usarlos en sus vistas. No hay solución para ninguna personalización seria de la membresía, pero si la pregunta original fuera necesaria solo para las vistas, entonces esto quizás sea suficiente. Lo siguiente se usó para verificar una variable devuelta por un authorizefilter, usado para verificar si algunos enlaces debían presentarse o no (no para ningún tipo de lógica de autorización o concesión de acceso).

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
            }
        }
    }

Luego, simplemente agregue una referencia en las áreas web.config y llámelo como se muestra a continuación en la vista.

@User.IsEditor()

respondido 09 mar '13, 23:03

En su solución, nuevamente necesitamos hacer llamadas a la base de datos cada vez. Porque el objeto de usuario no tiene propiedades personalizadas. Solo tiene nombre y está autenticado - unAmigoAgradable

Eso depende completamente de su implementación y comportamiento deseado. Mi muestra contiene 0 líneas de lógica de base de datos o función. Si uno usa IsInRole, creo que podría almacenarse en caché en una cookie. O implementa su propia lógica de almacenamiento en caché. - Base

Basado en La respuesta de LukePy agregue algunos métodos para configurar timeout y requireSSL cooperado con Web.config.

Los enlaces de referencias

Códigos modificados de lucasp

1, conjunto timeout basados en Web.Config. Las FormsAuthentication.Timeout obtendrá el valor de tiempo de espera, que se define en web.config. Envolví lo siguiente para que sea una función, que devuelve un ticket atrás.

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, configure la cookie para que sea segura o no, según la RequireSSL configuración.

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

contestado el 23 de mayo de 17 a las 13:05

Muy bien, soy un cuidador de criptas serio aquí al arrastrar esta pregunta muy antigua, pero hay un enfoque mucho más simple para esto, que fue mencionado por @Baserz anteriormente. Y eso es usar una combinación de métodos de extensión C # y almacenamiento en caché (NO use sesión).

De hecho, Microsoft ya ha proporcionado varias de estas extensiones en el Microsoft.AspNet.Identity.IdentityExtensions espacio de nombres. Por ejemplo, GetUserId() es un método de extensión que devuelve el ID de usuario. También hay GetUserName() y FindFirstValue(), que devuelve reclamaciones basadas en el IPrincipal.

Por lo tanto, solo necesita incluir el espacio de nombres y luego llamar User.Identity.GetUserName() para obtener el nombre de usuario configurado por ASP.NET Identity.

No estoy seguro de si esto está almacenado en caché, ya que la identidad ASP.NET anterior no es de código abierto y no me he molestado en realizar ingeniería inversa. Sin embargo, si no es así, puede escribir su propio método de extensión, que almacenará en caché este resultado durante un período de tiempo específico.

contestado el 31 de mayo de 17 a las 21:05

¿Por qué "no usar sesión"? - Alex

@jitbit: porque la sesión no es confiable e insegura. Por la misma razón, nunca debe utilizar session por motivos de seguridad. - Erik Funkenbusch

"No confiable" se puede solucionar volviendo a llenar la sesión (si está vacía). "No seguro": hay formas de protegerse del secuestro de sesiones (utilizando solo HTTPS y otras formas). Pero en realidad estoy de acuerdo contigo. ¿Dónde lo guardarías en caché entonces? Información como IsUserAdministrator or UserEmail etc.? Tu pensando HttpRuntime.Cache? - Alex

@jitbit: esa es una opción u otra solución de almacenamiento en caché si la tiene. Asegurarse de caducar la entrada de la caché después de un período de tiempo. Inseguro también se aplica al sistema local, ya que puede modificar manualmente la cookie y adivinar los ID de sesión. El hombre en el medio no es la única preocupación. - Erik Funkenbusch

Como una adición al código LukeP para usuarios de formularios Web Forms (no MVC) si desea simplificar el acceso en el código detrás de sus páginas, simplemente agregue el código a continuación a una página base y obtenga la página base en todas sus páginas:

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

Entonces, en su código detrás, simplemente puede acceder:

User.FirstName or User.LastName

Lo que me falta en un escenario de formulario web es cómo obtener el mismo comportamiento en el código no vinculado a la página, por ejemplo, en módulos http ¿Debo agregar siempre un yeso en cada clase o hay una forma más inteligente de obtenerlo?

Gracias por sus respuestas y gracias a LukeP ya que usé sus ejemplos como base para mi usuario personalizado (que ahora tiene User.Roles, User.Tasks, User.HasPath(int) , User.Settings.Timeout y muchas otras cosas bonitas)

Respondido 25 Jul 13, 14:07

Probé la solución sugerida por LukeP y descubrí que no es compatible con el atributo Authorize. Entonces, lo modifiqué un poco.

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);
    }

Y finalmente en 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;
        }
    }

Ahora puedo acceder a los datos en vistas y controladores simplemente llamando

User.ExInfo()

Para cerrar la sesión, solo llamo

AuthManager.SignOut();

donde es AuthManager

HttpContext.GetOwinContext().Authentication

Respondido el 26 de junio de 18 a las 10:06

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.