web-dev-qa-db-fra.com

ASP.NET MVC - Définir une identité personnalisée ou IPrincipal

J'ai besoin de faire quelque chose d'assez simple: dans mon application ASP.NET MVC, je souhaite définir un IIdentity/IPrincipal personnalisé. Selon ce qui est plus facile/plus approprié. Je veux étendre la valeur par défaut pour pouvoir 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 de rendre les choses plus difficiles qu'elles ne le sont réellement. Je pensais que ce serait facile. Si un utilisateur ouvre une session, je souhaite définir une identité IIdentity personnalisée. Alors j'ai pensé, je vais implémenter Application_PostAuthenticateRequest dans mon global.asax. Toutefois, cela est appelé à chaque demande et je ne souhaite pas faire d'appel à la base de données pour chaque demande qui demanderait toutes les données de la base de données et créerait 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ù proviendraient ces données?

J'ai donc pensé que chaque fois qu'un utilisateur se connectait, je pouvais ajouter quelques variables nécessaires à ma session, que j'ajoutais à l'identité personnalisée IIdentity dans le gestionnaire d'événements Application_PostAuthenticateRequest. Cependant, mon Context.Session est null, ce n'est donc pas la voie à suivre.

Je travaille dessus depuis un jour maintenant et j’ai le sentiment que quelque chose me manque. Cela ne devrait pas être trop difficile à faire, non? Je suis aussi un peu confus par tout ce qui est (semi) lié à ç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 une identité sans tout le fuzz supplémentaire… ce serait génial! Je sais qu'il y a des questions similaires sur SO, mais si la réponse dont j'ai besoin est là, je l'aurais peut-être oublié.

634
Razzie

Voici comment je le fais.

J'ai décidé d'utiliser IPrincipal au lieu de IIdentity car cela signifie que je n'ai pas à implémenter à la fois 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 la sérialisation d'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 de connexion - 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 en remplaçant 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 en vues rasoir

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

et en 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 User renvoyé (HttpContext.User):

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

et ensuite, pour chaque contrôleur:

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

ce qui vous permettra d'accéder à des champs personnalisés dans le code comme ceci:

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
823
LukeP

Je ne peux pas parler directement pour ASP.NET MVC, mais pour ASP.NET Web Forms, le truc consiste à créer un FormsAuthenticationTicket et à le chiffrer dans un cookie une fois l'utilisateur authentifié. De cette façon, il vous suffit d'appeler la base de données une seule fois (ou l'AD ou tout ce que vous utilisez pour effectuer votre authentification), et chaque requête ultérieure s'authentifiera en fonction du ticket stocké dans le cookie.

Un bon article à ce sujet: 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érerais également de remplacer la réponse acceptée par celle-ci.

Modifier 2: Une alternative au lien brisé: https://web.archive.org/web/20120422011422/http:// ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html

107
John Rasch

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 identifiant que je gère. Vous pouvez ajouter des informations supplémentaires telles que l'adresse e-mail aux données 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 le golbal asax, ajoutez 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 utiliserez 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

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 blog à ce sujet ici. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.

15
brady gaster

Voici une solution si vous devez 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 uniquement pour des vues, cela suffirait peut-être. Ce qui suit a été utilisé pour vérifier une variable renvoyée par un filtre d’autorisation, ainsi que pour vérifier si certains liens doivent être présentés ou non (pas pour tout type de 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
            }
        }
    }

Ajoutez simplement une référence dans les zones web.config et appelez-la comme ci-dessous dans la vue.

@User.IsEditor()
10
Base

Basé sur réponse de LukeP , et 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, Définissez timeout sur la base de Web.Config. Le FormsAuthentication.Timeout obtiendra la valeur de délai d'attente définie dans web.config. J'ai emballé les suivants comme une fonction, ce qui retourne un ticket.

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, Configurez le cookie pour qu’il soit sécurisé ou non, en fonction de la configuration RequireSSL.

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

Très bien, je suis donc un gardien de cryptage sérieux en soulevant cette très vieille question, mais il existe une approche beaucoup plus simple à cet égard, qui a été évoquée par @Baserz ci-dessus. Et cela consiste à utiliser une combinaison de méthodes d'extension C # et de mise en cache (ne pas utiliser de session).

En fait, Microsoft a déjà fourni un certain nombre de ces extensions dans l'espace de noms Microsoft.AspNet.Identity.IdentityExtensions . Par exemple, GetUserId() est une méthode d'extension qui renvoie l'identifiant de l'utilisateur. Il existe également GetUserName() et FindFirstValue(), qui renvoie les revendications en fonction de l'IPrincipal.

Vous devez donc uniquement inclure l'espace de noms, puis appeler User.Identity.GetUserName() pour obtenir le nom d'utilisateur configuré par ASP.NET Identity.

Je ne sais pas si cela est mis en cache, car l'ancienne ASP.NET Identity n'est pas à source ouverte et je n'ai pas pris la peine de procéder à un reverse engineering. Cependant, si ce n'est pas le cas, vous pouvez écrire votre propre méthode d'extension, ce qui mettra en cache ce résultat pendant une période donnée.

3
Erik Funkenbusch

En complément du code LukeP pour les utilisateurs de Web Forms (pas MVC), si vous souhaitez simplifier l'accès au code derrière vos pages, ajoutez simplement le code ci-dessous à une page de base et dérivez 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, vous pouvez simplement accéder à:

User.FirstName or User.LastName

Ce qui me manque dans un scénario de formulaire Web, c'est comment obtenir le même comportement dans un code non lié à la page, par exemple dans httpmodules devrais-je toujours ajouter un cast 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 bien d'autres choses intéressantes.)

2
Manight

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

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, il suffit d'appeler

AuthManager.SignOut();

où AuthManager est

HttpContext.GetOwinContext().Authentication
0
Vasily Ivanov