Pregunta ASP.NET MVC: establezca IIdentity personalizado o IPrincipal


Necesito hacer algo bastante simple: en mi aplicación ASP.NET MVC, quiero establecer un IIdentity / IPrincipal personalizado. Cualquiera que sea más fácil / más adecuado. Quiero extender el valor predeterminado para que pueda llamar algo así como User.Identity.Id y User.Identity.Role. Nada lujoso, 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 configurar un IIdentity personalizado. Entonces pensé, implementaré Application_PostAuthenticateRequest en mi global.asax. Sin embargo, eso se invoca 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 (hacer llamadas a la base de datos allí), pero podría estar equivocado. ¿O de dónde más provendrían esos datos?

Así que pensé, cada vez que un usuario inicia sesión, puedo agregar algunas variables necesarias en mi sesión, las cuales agrego al IIdentity personalizado en el Application_PostAuthenticateRequest controlador de eventos. Sin embargo, mi Context.Session es null allí, así que ese tampoco es el camino a seguir.

He estado trabajando en esto por 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 datos adicionales en un IIdentity sin toda la fuzz extra ... ¡sería genial! Sé que hay preguntas similares en SO, pero si la respuesta que necesito está allí, debo haberla pasado por alto.


595
2018-06-30 15:18


origen


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. 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: para serializar información personalizada en el campo userdata 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 LogIn: 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 las 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 es autoexplicativo. Si no es así, házmelo saber.

Además, para facilitar aún más el acceso, puede crear un controlador base y anular el objeto 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á dentro de las vistas. 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, puede acceder de esta manera:

@User.FirstName
@User.LastName

786
2018-05-09 21:24



No puedo hablar directamente para 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 ha sido autenticado. De esta forma, solo tiene que llamar a la base de datos una vez (o AD o lo que sea que esté utilizando para realizar su autenticación), y cada solicitud subsiguiente se autenticará en base al 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)

Editar:

Como el enlace anterior está roto, recomendaría la solución de LukeP en su respuesta anterior: https://stackoverflow.com/a/10524305 - También sugiero 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


105
2018-06-30 15:28



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.


63
2017-11-20 10:55



MVC le proporciona el método OnAuthorize que se cuelga de sus clases de controlador. O bien, podría usar 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


15
2018-06-23 12:21



Aquí hay una solución si necesita conectar algunos métodos a @User para utilizarlos en sus vistas. No hay solución para una personalización seria de la membresía, pero si la pregunta original era necesaria solo para vistas, entonces quizás sea suficiente. Lo siguiente se usó para verificar una variable devuelta desde un filtro de autorizaciones, que se usa para verificar si algunos enlaces se presentan 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 solo agrega una referencia en las áreas web.config, y llámala como abajo en la vista.

@User.IsEditor()

9
2018-03-09 23:10



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

Los enlaces de referencias

Códigos modificados de LukeP

1, conjunto timeout Residencia en Web.Config. los FormsAuthentication.Timeout obtendrá el valor de tiempo de espera, que se define en web.config. Envolví los siguientes para ser una función, que devuelve un ticket espalda.

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, en función del RequireSSL configuración.

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

3
2018-04-23 09:58



Está bien, entonces soy un críptico serio aquí arrastrando esta muy vieja pregunta, pero hay un enfoque mucho más simple para esto, que fue tocado por @Baserz arriba. Y eso es usar una combinación de métodos de extensión C # y almacenamiento en caché (NO usar sesión).

De hecho, Microsoft ya ha proporcionado una serie de tales extensiones en el Microsoft.AspNet.Identity.IdentityExtensions espacio de nombres Por ejemplo, GetUserId() es un método de extensión que devuelve el Id del usuario. También hay GetUserName() y FindFirstValue(), que devuelve reclamos basados ​​en IPrincipal.

Entonces solo necesita incluir el espacio de nombres y luego llamar User.Identity.GetUserName() para obtener el nombre de los usuarios según lo configurado por ASP.NET Identity.

No estoy seguro de si esto está almacenado en la memoria caché, ya que la identidad anterior de ASP.NET no está abierta, y no me he tomado la molestia de realizar una 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.


3
2018-05-31 20:49



Como complemento del código LukeP para usuarios de formularios web (no MVC) si desea simplificar el acceso en el código subyacente de sus páginas, simplemente agregue el código siguiente 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 a:

User.FirstName or User.LastName

Lo que me falta en un escenario de Web Form es cómo obtener el mismo comportamiento en código no vinculado a la página, por ejemplo en httpmodules ¿Debería siempre agregar un molde en cada clase o hay una forma más inteligente de obtener esto?

Gracias por sus respuestas y gracias a LukeP ya que utilicé 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 agradables)


2
2017-12-24 18:32



Probé la solución sugerida por LukeP y descubrí que no es compatible con el atributo Autorizar. 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 desconectarme solo llamo

AuthManager.SignOut();

donde está AuthManager

HttpContext.GetOwinContext().Authentication

0
2018-06-26 09:02