Question ASP.NET MVC - Définir IIdentity personnalisé ou IPrincipal


J'ai besoin de faire quelque chose d'assez simple: dans mon application ASP.NET MVC, je veux définir un IIdentity / IPrincipal personnalisé. Celui qui est le plus facile / le plus approprié. Je veux étendre la valeur par défaut afin que je puisse appeler quelque chose comme User.Identity.Id et User.Identity.Role. Rien d'extraordinaire, juste quelques propriétés supplémentaires.

J'ai lu des tonnes d'articles et de questions, mais j'ai l'impression que je le rends plus difficile qu'il ne l'est en réalité. Je pensais que ce serait facile. Si un utilisateur se connecte, je veux définir un IIdentity personnalisé. Alors j'ai pensé, je vais mettre en œuvre Application_PostAuthenticateRequest dans mon global.asax. Cependant, cela est appelé sur chaque requête, et je ne veux pas faire un appel à la base de données sur chaque requête qui demanderait toutes les données de la base de données et mettrait dans un objet IPrincipal personnalisé. Cela semble également très inutile, lent, et au mauvais endroit (faire des appels de base de données là-bas) mais je peux me tromper. Ou d'où viendraient ces données?

Donc, je pensais, chaque fois qu'un utilisateur se connecte, je peux ajouter quelques variables nécessaires dans ma session, que j'ajoute à la IIdentity personnalisé dans le Application_PostAuthenticateRequest gestionnaire d'événements. Cependant, mon Context.Session est null là-bas, ce n'est donc pas le chemin à parcourir.

Je travaille là-dessus depuis un jour et je sens qu'il me manque quelque chose. Cela ne devrait pas être trop difficile à faire, non? Je suis aussi un peu confus par tous les trucs (semi) liés à ça. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Suis-je le seul qui trouve tout cela très déroutant?

Si quelqu'un pouvait me dire une solution simple, élégante et efficace pour stocker des données supplémentaires sur un IIdentity sans tous les fuzz supplémentaires .. ce serait génial! Je sais qu'il y a des questions similaires sur le SO, mais si la réponse dont j'ai besoin est là, je dois l'avoir oublié.


595
2018-06-30 15:18


origine


Réponses:


Voici comment je le fais.

J'ai décidé d'utiliser IPrincipal au lieu de IIdentity car cela signifie que je n'ai pas besoin d'implémenter IIdentity et IPrincipal.

  1. Créer l'interface

    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 - pour sérialiser les informations personnalisées dans le champ userdata de l'objet FormsAuthenticationTicket.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
  4. Méthode LogIn - Configuration d'un cookie avec des informations personnalisées

    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 - Lecture du cookie et remplacement de l'objet HttpContext.User, ceci est effectué en surchargeant 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. Accès aux vues Razor

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

et dans le code:

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

Je pense que le code est explicite. Si ce n'est pas le cas, faites le moi savoir.

De plus, pour rendre l'accès encore plus facile, vous pouvez créer un contrôleur de base et remplacer l'objet Utilisateur retourné (HttpContext.User):

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

puis, pour chaque contrôleur:

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

ce qui vous permettra d'accéder à des champs personnalisés dans un code comme celui-ci:

User.Id
User.FirstName
User.LastName

Mais cela ne fonctionnera pas à l'intérieur des vues. Pour cela, vous devez créer une implémentation WebViewPage personnalisée:

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

Faites-en un type de page par défaut dans 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>

et dans les vues, vous pouvez y accéder comme ceci:

@User.FirstName
@User.LastName

786
2018-05-09 21:24



Je ne peux pas parler directement pour ASP.NET MVC, mais pour ASP.NET Web Forms, l'astuce consiste à créer un FormsAuthenticationTicket et cryptez-le dans un cookie une fois que l'utilisateur a été authentifié. De cette façon, vous n'avez qu'à appeler la base de données une fois (ou AD ou tout ce que vous utilisez pour effectuer votre authentification), et chaque requête suivante s'authentifiera en fonction du ticket stocké dans le cookie.

Un bon article sur ceci: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (lien brisé)

Modifier:

Puisque le lien ci-dessus est cassé, je recommanderais la solution de LukeP dans sa réponse ci-dessus: https://stackoverflow.com/a/10524305 - Je suggère également que la réponse acceptée soit changée en celle-là.

Édition 2: Une alternative pour le lien cassé: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html


105
2018-06-30 15:28



Voici un exemple pour faire le travail. bool isValid est défini en regardant un magasin de données (disons votre base de données utilisateur). UserID est juste un ID que je maintiens. Vous pouvez ajouter des informations supplémentaires telles que l'adresse e-mail aux données de l'utilisateur.

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

dans l'asax golbal ajouter le code suivant pour récupérer vos informations

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

Lorsque vous allez utiliser les informations ultérieurement, vous pouvez accéder à votre principal personnalisé comme suit.

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

Cela vous permettra d'accéder à des informations utilisateur personnalisées.


63
2017-11-20 10:55



MVC vous fournit la méthode OnAuthorize qui se bloque à partir de vos classes de contrôleur. Vous pouvez également utiliser un filtre d'action personnalisé pour effectuer une autorisation. MVC le rend assez facile à faire. J'ai posté un article de blog à ce sujet ici. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0


15
2018-06-23 12:21



Voici une solution si vous avez besoin de connecter certaines méthodes à @User pour les utiliser dans vos vues. Aucune solution pour une personnalisation sérieuse des membres, mais si la question initiale était nécessaire pour les vues seules, cela suffirait peut-être. Ce qui suit a été utilisé pour vérifier une variable renvoyée par un authorizefilter, utilisée pour vérifier si certains liens devaient être présentés ou non (pas pour une quelconque logique d'autorisation ou d'octroi d'accès).

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

Il suffit ensuite d'ajouter une référence dans les zones web.config, et de l'appeler comme ci-dessous dans la vue.

@User.IsEditor()

9
2018-03-09 23:10



Basé sur La réponse de LukePet ajoutez quelques méthodes pour configurer timeout et requireSSL coopéré avec Web.config.

Les liens de références

Codes modifiés de LukeP

1 jeu timeout basé sur Web.Config. le FormsAuthentication.Timeout obtiendra la valeur de timeout, qui est définie dans web.config. J'ai enveloppé les suivants pour être une fonction, qui retourne un ticket arrière.

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, configurer le cookie pour être sécurisé ou non, basé sur le RequireSSL configuration.

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



D'accord, donc je suis un cryptkeeper sérieux ici en faisant glisser cette très vieille question, mais il y a une approche beaucoup plus simple à ce sujet, qui a été abordée par @Baserz ci-dessus. Et cela consiste à utiliser une combinaison de méthodes d'extension C # et de mise en cache (N'utilisez PAS de session).

En fait, Microsoft a déjà fourni un certain nombre de ces extensions dans le Microsoft.AspNet.Identity.IdentityExtensions espace de nommage. Par exemple, GetUserId() est une méthode d'extension qui renvoie l'identifiant de l'utilisateur. Il y a aussi GetUserName() et FindFirstValue(), qui renvoie les revendications basées sur IPrincipal.

Il suffit donc d'inclure l'espace de nom, puis d'appeler User.Identity.GetUserName() pour obtenir le nom des utilisateurs configuré par ASP.NET Identity.

Je ne suis pas certain si cela est mis en cache, car l'identité ASP.NET est plus ouverte ne provient pas, et je ne l'ai pas pris la peine de désossage. Cependant, si ce n'est pas le cas, vous pouvez écrire votre propre méthode d'extension, qui mettra ce résultat en cache pendant un certain temps.


3
2018-05-31 20:49



En complément au code LukeP pour les utilisateurs de Web Forms (non MVC) si vous souhaitez simplifier l'accès dans le code derrière de vos pages, il suffit d'ajouter le code ci-dessous pour une page de base et d'en tirer la page de base dans toutes vos pages:

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

Donc, dans votre code derrière vous pouvez simplement accéder:

User.FirstName or User.LastName

Ce qui me manque dans un scénario de formulaire Web, est comment obtenir le même comportement dans le code non lié à la page, par exemple dans httpmodules devrais-je toujours ajouter une distribution dans chaque classe ou existe-t-il un moyen plus intelligent d'obtenir ceci?

Merci pour vos réponses et merci à LukeP depuis que j'ai utilisé vos exemples comme base pour mon utilisateur personnalisé (qui a maintenant User.Roles, User.Tasks, User.HasPath(int) , User.Settings.Timeout et beaucoup d'autres belles choses)


2
2017-12-24 18:32



J'ai essayé la solution proposée par LukeP et j'ai constaté qu'elle ne supporte pas l'attribut Authorize. Donc, je l'ai modifié un peu.

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

Et enfin dans 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;
        }
    }

Maintenant, je peux accéder aux données dans les vues et les contrôleurs simplement en appelant

User.ExInfo()

Pour me déconnecter, je viens d'appeler

AuthManager.SignOut();

où AuthManager est

HttpContext.GetOwinContext().Authentication

0
2018-06-26 09:02