ASP.NET MVC-Set custom IIdentity or IPrincipal

Muszę zrobić coś dość prostego: w moim ASP.NET aplikacja MVC, chcę ustawić niestandardowe IIdentity / IPrincipal. Co jest łatwiejsze / bardziej odpowiednie. Chcę rozszerzyć domyślne tak, że mogę wywołać coś w rodzaju User.Identity.Id i User.Identity.Role. Nic wymyślnego, tylko dodatkowe właściwości.

Przeczytałem mnóstwo artykułów i pytań, ale czuję, że utrudniam to, niż jest w rzeczywistości. Myślałem, że to będzie łatwe. Jeśli użytkownik loguje się, chcę ustawić niestandardową IIdentity. Więc pomyślałem, zaimplementuje Application_PostAuthenticateRequest w moim globalnym.asax. Jednak jest to wywoływane przy każdym żądaniu i nie chcę wykonywać wywołania do bazy danych przy każdym żądaniu, które zażądałoby wszystkich danych z bazy danych i umieściłoby w niestandardowym obiekcie IPrincipal. To też wydaje się bardzo niepotrzebne, powolne i w złym miejscu (Robienie tam połączeń z bazami danych), ale mogę się mylić. Albo skąd by się wzięły te dane?

Więc pomyślałem, że za każdym razem, gdy loguje się użytkownik, Mogę dodać kilka niezbędnych zmiennych w mojej sesji, które Dodaję do custom IIdentity w Application_PostAuthenticateRequest event handler. Jednak mój Context.Session jest null tam, więc to też nie jest droga.

Pracuję nad tym od jednego dnia i czuję, że coś mi umyka. To nie powinno być trudne, prawda? Jestem również nieco zdezorientowany wszystkimi (częściowo) związanymi z tym rzeczami. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Czy tylko ja uważam to za bardzo mylące?

Gdyby ktoś mógł mi powiedzieć prosty, elegancki, i wydajne rozwiązanie do przechowywania dodatkowych danych na IIdentity bez wszystkich dodatkowych fuzz.. byłoby świetnie! Wiem, że są podobne pytania na SO, ale jeśli odpowiedź, której potrzebuję, jest tam, musiałem przeoczyć.

Author: Kiquenet, 2009-06-30

9 answers

Oto Jak to robię.

Zdecydowałem się użyć IPrincipal zamiast IIdentity, ponieważ oznacza to, że nie muszę implementować zarówno IIdentity, jak i IPrincipal.

  1. Tworzenie interfejsu

    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 - służy do serializacji niestandardowych informacji do pola userdata w obiekcie FormsAuthenticationTicket.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
  4. Metoda logowania-konfigurowanie pliku cookie z niestandardowym informacje

    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. Globalny.asax.cs-odczyt pliku cookie i zastąpienie HttpContext.Obiekt użytkownika, odbywa się to poprzez nadpisanie 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. Access in Razor views

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

I w kodzie:

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

Myślę, że kod jest oczywisty. Jeśli nie, daj mi znać.

Dodatkowo, aby ułatwić dostęp, można utworzyć kontroler bazowy i nadpisać zwracany obiekt użytkownika (HttpContext.Użytkownik):

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

A następnie dla każdego kontrolera:

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

Który pozwoli ci uzyskać dostęp do niestandardowych pól w kodzie w następujący sposób:

User.Id
User.FirstName
User.LastName

Ale to nie zadziała wewnątrz widoków. W tym celu należy utworzyć niestandardową implementację 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; }
    }
}

Ustaw domyślny typ strony w 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>

I w widokach, możesz uzyskać do niego dostęp w następujący sposób:

@User.FirstName
@User.LastName
 799
Author: LukeP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-31 17:36:41

I can ' t speak directly for ASP.NET MVC, ale dla ASP.NET formularze internetowe, sztuczka polega na utworzeniu FormsAuthenticationTicket i zaszyfrowaniu go w pliku cookie po uwierzytelnieniu użytkownika. W ten sposób wystarczy tylko raz zadzwonić do bazy danych (lub reklamy lub czegokolwiek, czego używasz do uwierzytelniania), a każde kolejne żądanie uwierzytelni się na podstawie biletu zapisanego w pliku cookie.

Dobry artykuł na ten temat: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (broken link)

Edit:

Ponieważ powyższy link jest uszkodzony, polecam rozwiązanie Lukepa w jego odpowiedzi powyżej: https://stackoverflow.com/a/10524305 - sugerowałbym również zmianę przyjętej odpowiedzi na tę.

Edit 2: Alternatywa dla zepsutego linku: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html

 105
Author: John Rasch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:41

Oto przykład, aby wykonać zadanie. bool isValid ustawia się patrząc na jakiś magazyn danych (powiedzmy bazę danych użytkownika). UserID to tylko identyfikator, który utrzymuję. Możesz dodać dodatkowe informacje, takie jak adres e-mail do danych użytkownika.

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

W golbal asax Dodaj następujący kod, aby pobrać swoje informacje

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

Kiedy zamierzasz użyć informacji później, możesz uzyskać dostęp do swojego niestandardowego zleceniodawcy w następujący sposób.

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

To pozwoli ci uzyskać dostęp do niestandardowych informacje o użytkowniku.

 63
Author: Sriwantha Attanayake,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-06 19:35:43

MVC dostarcza ci metodę OnAuthorize, która zawiesza się z Twoich klas kontrolerów. Możesz też użyć niestandardowego filtra akcji do autoryzacji. MVC sprawia, że jest to dość łatwe do zrobienia. Zamieściłem tutaj wpis na blogu o tym. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0

 15
Author: brady gaster,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-07 23:37:54

Oto rozwiązanie, jeśli chcesz podłączyć niektóre metody do @User do użytku w swoich widokach. Nie ma rozwiązania dla jakiejkolwiek poważnej personalizacji członkostwa, ale jeśli pierwotne pytanie było potrzebne tylko dla poglądów, to może to wystarczyć. Poniższy plik został użyty do sprawdzenia zmiennej zwróconej z filtra autoryzacyjnego, służącego do sprawdzenia, czy niektóre linki mają być prezentowane, czy nie(nie dla jakiejkolwiek logiki autoryzacji lub przyznania dostępu).

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

Następnie wystarczy dodać odniesienie w obszarach www.config i wywołaj to jak poniżej w widoku.

@User.IsEditor()
 9
Author: Baseless,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-09 23:10:04

Na podstawie odpowiedzi Lukepa i dodać kilka metod do konfiguracji timeout i requireSSL.

Odnośniki

Zmodyfikowane kody LukeP

1, Zestaw timeout na podstawie Web.Config. Na FormsAuthentication.Timeout otrzyma wartość timeout, która jest zdefiniowana w web.config. I owinięte następujące funkcje, które zwracają ticket z powrotem.

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, Skonfiguruj plik cookie, aby był Bezpieczny lub nie, na podstawie konfiguracji RequireSSL.

HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
// respect to `RequreSSL` in `Web.Config`
bool bSSL = FormsAuthentication.RequireSSL;
faCookie.Secure = bSSL;
 3
Author: AechoLiu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:25

W porządku, więc jestem poważnym kryptowalutą, przeciągając to bardzo stare pytanie, ale jest dużo prostsze podejście do tego, co zostało poruszone przez @Baserz powyżej. A to jest użycie kombinacji metod rozszerzenia C# i buforowania (nie używaj sesji).

W rzeczywistości Microsoft udostępnił już kilka takich rozszerzeń w Microsoft.AspNet.Identity.IdentityExtensions przestrzeń nazw. Na przykład, {[1] } jest metodą rozszerzenia, która zwraca identyfikator użytkownika. Istnieje również GetUserName() i FindFirstValue(), które zwracają roszczenia oparte na IPrincipal.

Więc musisz tylko dołączyć przestrzeń nazw, a następnie wywołać User.Identity.GetUserName(), Aby uzyskać nazwę użytkownika skonfigurowaną przez ASP.NET tożsamość.

Nie jestem pewien, czy jest to buforowane, ponieważ starsze ASP.NET tożsamość nie jest open source, i nie zadałem sobie trudu, aby ją odtworzyć. Jeśli jednak tak nie jest, możesz napisać własną metodę rozszerzenia, która będzie buforować ten wynik przez określony czas.

 3
Author: Erik Funkenbusch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-31 20:49:11

Jako dodatek do kodu LukeP dla użytkowników formularzy internetowych (nie MVC) jeśli chcesz uprościć dostęp do kodu za swoimi stronami, po prostu dodaj poniższy kod do strony bazowej i uzyskaj stronę bazową na wszystkich swoich stronach:

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

Więc w Twoim kodzie za możesz po prostu uzyskać dostęp:

User.FirstName or User.LastName

To, czego mi brakuje w scenariuszu formularzy internetowych, to sposób uzyskania tego samego zachowania w kodzie nie związanym ze stroną, na przykład w httpmodules Czy zawsze powinienem dodać obsadę w każdej klasie lub jest jest mądrzejszy sposób, aby to uzyskać?

Dzięki za odpowiedzi i dziękuję Lukepowi, ponieważ użyłem Twoich przykładów jako bazy dla mojego niestandardowego użytkownika (który teraz ma User.Roles, User.Tasks, User.HasPath(int) , User.Settings.Timeout i wiele innych fajnych rzeczy)

 2
Author: Manight,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-25 13:56:15

Wypróbowałem rozwiązanie zaproponowane przez LukeP i okazało się, że nie obsługuje atrybutu Authorize. Trochę go zmodyfikowałem.

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

I wreszcie w 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;
        }
    }

Teraz mogę uzyskać dostęp do danych w widokach i kontrolerach po prostu przez wywołanie

User.ExInfo()

Aby się wylogować wystarczy zadzwonić

AuthManager.SignOut();

Gdzie authmanager jest

HttpContext.GetOwinContext().Authentication
 0
Author: Vasily Ivanov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-26 09:02:53