ASP.net MVC - defina IIdentity personalizado ou IPrincipal

650

Preciso fazer algo bastante simples: no meu aplicativo ASP.NET MVC, quero definir um IIdentity / IPrincipal personalizado. O que for mais fácil / mais adequado. Eu quero estender o padrão para que eu possa chamar algo como User.Identity.Ide User.Identity.Role. Nada extravagante, apenas algumas propriedades extras.

Eu li vários artigos e perguntas, mas sinto que estou dificultando o que realmente é. Eu pensei que seria fácil. Se um usuário fizer logon, desejo definir uma identidade II personalizada. Então eu pensei, vou implementar Application_PostAuthenticateRequestno meu global.asax. No entanto, isso é chamado em todas as solicitações, e eu não quero fazer uma chamada ao banco de dados em todas as solicitações que solicitariam todos os dados do banco de dados e colocassem um objeto IPrincipal personalizado. Isso também parece muito desnecessário, lento e no lugar errado (fazendo chamadas de banco de dados lá), mas eu posso estar errado. Ou de onde mais esses dados viriam?

Por isso, pensei que, sempre que um usuário efetua login, posso adicionar algumas variáveis ​​necessárias em minha sessão, adicionadas à IIdentity personalizada no Application_PostAuthenticateRequestmanipulador de eventos. No entanto, o meu Context.Sessionestá nulllá, de modo que também não é o caminho a percorrer.

Estou trabalhando nisso há um dia e sinto que estou perdendo alguma coisa. Isso não deve ser muito difícil de fazer, certo? Também estou um pouco confuso com todas as coisas (semi) relacionadas que acompanham isso. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Eu sou o único que acha tudo isso muito confuso?

Se alguém pudesse me dizer uma solução simples, elegante e eficiente para armazenar alguns dados extras em uma identidade sem todo o fuzz extra ... isso seria ótimo! Eu sei que existem perguntas semelhantes no SO, mas se a resposta que eu preciso está aí, eu devo ter esquecido.

Razzie
fonte
1
Oi Domi, é uma combinação de apenas armazenar dados que nunca mudam (como um ID de usuário) ou atualizar o cookie diretamente depois que o usuário altera os dados que precisam ser refletidos no cookie imediatamente. Se um usuário fizer isso, simplesmente atualizo o cookie com os novos dados. Mas tento não armazenar dados que mudam frequentemente.
Razzie
26
Esta pergunta tem 36k visualizações e muitos votos positivos. isso é realmente um requisito tão comum - e, nesse caso, não existe uma maneira melhor do que todas essas 'coisas personalizadas'?
6133 Simon_Weaver
2
@Simon_Weaver Existe um conhecimento de identidade do ASP.NET, que suporta informações personalizadas adicionais no cookie criptografado com mais facilidade.
John
1
Concordo com você, o que há para tanta informação como você postou: MemberShip..., Principal, Identity. O ASP.NET deve facilitar, simplificar e, no máximo, duas abordagens para lidar com a autenticação.
banda larga
1
@ Simon_Weaver Isso mostra claramente que há demanda por um sistema de identidade mais simples, mais fácil e flexível.
Niico 15/04

Respostas:

838

Aqui está como eu faço isso.

Decidi usar o IPrincipal em vez do IIdentity porque significa que não preciso implementar o IIdentity e o IPrincipal.

  1. Crie a 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 - para serializar informações personalizadas no campo de dados do usuário no objeto FormsAuthenticationTicket.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  4. Método de logon - configurando um cookie com informações personalizadas

    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 - Lendo o cookie e substituindo o objeto HttpContext.User, isso é feito substituindo 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. Acesso às visualizações do Razor

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

e no código:

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

Eu acho que o código é auto-explicativo. Se não for, me avise.

Além disso, para tornar o acesso ainda mais fácil, você pode criar um controlador base e substituir o objeto Usuário retornado (HttpContext.User):

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

e depois, para cada controlador:

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

que permitirá acessar campos personalizados em códigos como este:

User.Id
User.FirstName
User.LastName

Mas isso não funcionará dentro das visualizações. Para isso, você precisará criar uma implementação personalizada do 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; }
    }
}

Crie um tipo de página padrão em 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 nas visualizações, você pode acessá-lo assim:

@User.FirstName
@User.LastName
LukeP
fonte
9
Boa implementação; atente para RoleManagerModule substituindo seu principal personalizado por um RolePrincipal. Isso me causou muita dor - stackoverflow.com/questions/10742259/...
David Keaveny
9
ok, encontrei a solução, basta adicionar uma opção else que passe "" (string vazia), pois o email e a identidade serão anônimos.
Pierre-Alain Vigeant
3
DateTime.Now.AddMinutes (N) ... como fazer isso para não desconectar o usuário após N minutos, o usuário conectado pode persistir (quando o usuário marcar 'Lembrar-me', por exemplo)?
1110
4
Se você estiver usando o WebApiController, você precisará definir Thread.CurrentPrincipala Application_PostAuthenticateRequestpara que ele funcione como ele não dependemHttpContext.Current.User
Jonathan Levison
3
@AbhinavGujjar FormsAuthentication.SignOut();funciona bem para mim.
LukeP
109

Não posso falar diretamente para o ASP.NET MVC, mas para o ASP.NET Web Forms, o truque é criar FormsAuthenticationTickete criptografá-lo em um cookie depois que o usuário for autenticado. Dessa forma, você só precisa chamar o banco de dados uma vez (ou o AD ou o que estiver usando para executar sua autenticação) e cada solicitação subsequente será autenticada com base no ticket armazenado no cookie.

Um bom artigo sobre isso: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (link quebrado)

Editar:

Como o link acima está quebrado, eu recomendaria a solução de LukeP em sua resposta acima: https://stackoverflow.com/a/10524305 - Eu também sugeriria que a resposta aceita fosse alterada para essa.

Edição 2: Uma alternativa para o link quebrado: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html

John Rasch
fonte
Vindo do PHP, sempre coloquei as informações como UserID e outras peças necessárias para conceder acesso restrito na Session. Armazená-lo do lado do cliente me deixa nervoso, você pode comentar por que isso não será um problema?
John Zumbrum
@JohnZ - o próprio ticket é criptografado no servidor antes de ser enviado por fio, portanto, não é como se o cliente tivesse acesso aos dados armazenados no ticket. Observe que os IDs da sessão também são armazenados em um cookie, portanto, não é tão diferente assim.
31812 John Rasch
3
Se você está aqui você deve olhar de LukeP solução
mynkow
2
Sempre me preocupei com o potencial de exceder o tamanho máximo de cookie ( stackoverflow.com/questions/8706924/… ) com essa abordagem. Costumo usar o Cachecomo um Sessionsubstituto para manter os dados no servidor. Alguém pode me dizer se esta é uma abordagem falho?
Red Taz
2
Boa abordagem. Um problema em potencial com isso é se o objeto do usuário tiver mais do que algumas propriedades (e especialmente se houver objetos aninhados), a criação do cookie falhará silenciosamente quando o valor criptografado ultrapassar 4KB (é muito mais fácil de acertar do que você imagina). Se você armazenar apenas os principais dados, tudo bem, mas você precisará pressionar o DB ainda para o resto. Outra consideração é "atualizar" os dados do cookie quando o objeto do usuário tem alterações de assinatura ou lógica.
Geoffrey Hudik
63

Aqui está um exemplo para fazer o trabalho. O bool isValid é definido olhando para algum armazenamento de dados (digamos, sua base de dados do usuário). UserID é apenas um ID que estou mantendo. Você pode adicionar informações adicionais, como endereço de email, aos dados do usuário.

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

no golax asax, adicione o seguinte código para recuperar suas informações

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 você usar as informações posteriormente, poderá acessar seu principal personalizado da seguinte maneira.

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

isso permitirá que você acesse informações personalizadas do usuário.

Sriwantha Attanayake
fonte
2
FYI - it's Request.Cookies [] (plural)
Dan Esparza
10
Não se esqueça de definir Thread.CurrentPrincipal e Context.User como CustomPrincipal.
Russ Cam
6
De onde vem GetUserIdentity ()?
Ryan
Como mencionei no comentário, ele fornece uma implementação do System.Web.Security.IIdentity. Google sobre essa interface
Sriwantha Attanayake
16

O MVC fornece o método OnAuthorize que trava nas suas classes de controlador. Ou você pode usar um filtro de ação personalizado para executar a autorização. O MVC facilita bastante a execução. Eu publiquei um post sobre isso aqui. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0

brady gaster
fonte
Mas a sessão pode ser perdida e o usuário ainda se autentica. Não ?
Dragouf 18/08/19
@brady gaster, eu li a sua postagem no blog (obrigado!), Por que alguém usaria a substituição "OnAuthorize ()", como mencionado em sua postagem, na entrada global.asax "... AuthenticateRequest (..)" mencionada pela outra respostas? Um prefere o outro ao definir o usuário principal?
precisa saber é o seguinte
10

Aqui está uma solução se você precisar conectar alguns métodos ao @User para usar em suas visualizações. Nenhuma solução para qualquer personalização séria de associação, mas se a pergunta original fosse necessária apenas para visualizações, talvez isso fosse suficiente. O abaixo foi usado para verificar uma variável retornada de um filtro de autor, usada para verificar se alguns links devem ser apresentados ou não (não para qualquer tipo de lógica de autorização ou concessão de acesso).

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

Em seguida, basta adicionar uma referência nas áreas web.config e chamá-la como abaixo na exibição.

@User.IsEditor()
Base
fonte
1
Na sua solução, precisamos novamente fazer chamadas de banco de dados sempre. Porque o objeto de usuário não possui propriedades personalizadas. Só tem Name e IsAuthanticated
oneNiceFriend 5/16
Isso depende inteiramente da sua implementação e do comportamento desejado. Minha amostra contém 0 linhas de banco de dados, ou função, lógica. Se alguém usar o IsInRole, por sua vez, poderia ser armazenado em cache no cookie, eu acredito. Ou você implementa sua própria lógica de cache.
Base
3

Baseado em resposta de LukeP , adicione alguns métodos para configurar timeoute requireSSLcolaborar Web.config.

Os links de referências

Códigos modificados de LukeP

1, definido com timeoutbase Web.Config. O FormsAuthentication.Timeout obterá o valor do tempo limite, definido em web.config. Embrulhei o seguinte para ser uma função, que retorna uma ticketvolta.

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 o cookie para ser seguro ou não, com base na RequireSSLconfiguração.

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

Tudo bem, então eu sou um cryptkeeper sério arrastando essa pergunta muito antiga, mas há uma abordagem muito mais simples disso, que foi abordada por @Baserz acima. E isso é usar uma combinação de métodos de extensão C # e cache (NÃO use session).

De fato, a Microsoft já forneceu várias dessas extensões no Microsoft.AspNet.Identity.IdentityExtensionsespaço para nome. Por exemplo, GetUserId()é um método de extensão que retorna o ID do usuário. Há também GetUserName()eFindFirstValue() , que retorna declarações com base no IPrincipal.

Então você só precisa incluir o espaço para nome e chamar User.Identity.GetUserName() para obter o nome de usuário conforme configurado pela identidade do ASP.NET.

Não tenho certeza se isso está em cache, pois a antiga identidade do ASP.NET não é de código aberto e não me preocupei em fazer a engenharia reversa. No entanto, se não for, você poderá escrever seu próprio método de extensão, que armazenará esse resultado em cache por um período específico de tempo.

Erik Funkenbusch
fonte
Por que "não usar sessão"?
8777 Alex
@ jitbit - porque a sessão não é confiável e insegura. Pelo mesmo motivo, você nunca deve usar a sessão para fins de segurança.
Erik Funkenbusch 8/06
"Não confiável" pode ser resolvido repovoando a sessão (se vazia). "Não seguro" - existem maneiras de proteger contra o seqüestro de sessão (usando somente HTTPS + outras maneiras). Mas eu realmente concordo com você. Onde você o armazenaria em cache então? Informações como IsUserAdministratorou UserEmailetc.? Você está pensando HttpRuntime.Cache?
8777 Alex
@ jitbit - Essa é uma opção ou outra solução de cache, se você a tiver. Certifique-se de expirar a entrada do cache após um período de tempo. Inseguro também se aplica ao sistema local, pois você pode alterar manualmente o cookie e adivinhar os IDs da sessão. O homem do meio não é a única preocupação.
Erik Funkenbusch 8/17
2

Como um complemento ao código LukeP para usuários de Web Forms (não MVC), se você deseja simplificar o acesso no código por trás de suas páginas, basta adicionar o código abaixo a uma página base e derivar a página base em todas as suas páginas:

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

Portanto, no seu código por trás, você pode simplesmente acessar:

User.FirstName or User.LastName

O que estou perdendo em um cenário de formulário da Web é como obter o mesmo comportamento no código não vinculado à página, por exemplo, em httpmodules , devo sempre adicionar uma em cada classe ou existe uma maneira mais inteligente de obter isso?

Obrigado por suas respostas e agradecer a LukeP desde que eu usei seus exemplos como base para o meu usuário personalizada (que agora tem User.Roles, User.Tasks, User.HasPath(int), User.Settings.Timeoute muitas outras coisas boas)

Manight
fonte
0

Tentei a solução sugerida por LukeP e constatei que ela não suporta o atributo Autorizar. Então, eu modifiquei um pouco.

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

Agora eu posso acessar os dados em visualizações e controladores simplesmente chamando

User.ExInfo()

Para sair, eu ligo

AuthManager.SignOut();

onde AuthManager é

HttpContext.GetOwinContext().Authentication
Vasily Ivanov
fonte