ASP.NET MVC - встановлення користувальницької IIdentity або IPrincipal


650

Мені потрібно зробити щось досить просте: у своєму додатку ASP.NET MVC я хочу встановити користувальницьку IIdentity / IPrincipal. Що б легше / придатніше. Я хочу розширити за замовчуванням, щоб я міг викликати щось на кшталт User.Identity.Idі User.Identity.Role. Нічого фантазійного, лише деякі зайві властивості.

Я прочитав багато статей і питань, але відчуваю, що мені це важче, ніж є насправді. Я думав, що це буде легко. Якщо користувач входить у систему, я хочу встановити спеціальну IIdentity. Тому я подумав, що буду реалізовувати Application_PostAuthenticateRequestу своєму global.asax. Однак, це викликається в кожному запиті, і я не хочу робити дзвінки в базу даних за кожним запитом, який вимагає всіх даних з бази даних і вводить у користувацький об'єкт IPrincipal. Це також здається дуже непотрібним, повільним, і в неправильному місці (виконуючи дзвінки до бази даних), але я можу помилитися. Або звідки ще беруться ці дані?

Тому я думав, щоразу, коли користувач увійде в систему, я можу додати кілька необхідних змінних у своєму сеансі, які я додаю до користувацької IIdentity в Application_PostAuthenticateRequestобробнику подій. Однак моє Context.Sessionє null, тож це теж не шлях.

Я працюю над цим вже день і відчуваю, що чогось мені не вистачає. Це не повинно бути занадто важким, чи не так? Я також трохи збентежений усіма (напів) пов'язаними матеріалами, які поставляються з цим. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Am Я єдиний, хто знаходить все це дуже заплутаним?

Якби хтось міг сказати мені просте, елегантне та ефективне рішення для зберігання додаткових даних на IIdentity без зайвих нечітких .. це було б чудово! Я знаю, що є подібні питання щодо ТА, але якщо відповідь, що мені потрібна, є там, я, мабуть, не помітив уваги.


1
Привіт Домі, це поєднання лише зберігання даних, які ніколи не змінюються (як ідентифікатор користувача) або оновлення файлу cookie безпосередньо після того, як користувач змінює дані, які повинні бути відображені в файлі cookie відразу. Якщо користувач робить це, я просто оновлюю файли cookie новими даними. Але я намагаюся не зберігати дані, які часто змінюються.
Razzie

26
це питання має 36 тис. переглядів та багато відгуків. чи справді це загальна вимога - і якщо так, чи не існує кращого способу, ніж усі ці «спеціальні речі»?
Simon_Weaver

2
@Simon_Weaver Існує інформація про особу ASP.NET, яка легше підтримує додаткову користувацьку інформацію в зашифрованому файлі cookie.
Джон

1
Я згоден з вами, що потрібно багато інформації , як ви писали: MemberShip..., Principal, Identity. ASP.NET повинен зробити цей простіший, простіший і максимум два підходи для роботи з аутентифікацією.
широкосмуговий

1
@Simon_Weaver Це чітко показує, що є попит на більш просту, більш гнучку систему ідентифікації IMHO.
niico

Відповіді:


838

Ось як я це роблю.

Я вирішив використовувати IPrincipal замість IIdentity, оскільки це означає, що мені не потрібно реалізовувати і IIdentity, і IPrincipal.

  1. Створіть інтерфейс

    interface ICustomPrincipal : IPrincipal
    {
        int Id { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }
  2. Спеціальний принцип

    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 - для серіалізації користувацької інформації в поле даних про користувача в об’єкті FormsAuthenticationTicket.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  4. Метод входу - налаштування файлу cookie з власною інформацією

    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 - Читання файлів cookie та заміна об'єкта HttpContext.User, це робиться шляхом переопределення 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. Доступ у видах Razor

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

і в коді:

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

Я вважаю, що код пояснює себе. Якщо це не так, дайте мені знати.

Крім того, щоб зробити доступ ще простішим, ви можете створити базовий контролер і змінити повернутий об'єкт User (HttpContext.User):

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

а потім для кожного контролера:

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

що дозволить вам отримати доступ до спеціальних полів у такому коді:

User.Id
User.FirstName
User.LastName

Але це не спрацює всередині поглядів. Для цього вам потрібно створити власну реалізацію 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; }
    }
}

Зробіть це типом сторінки за замовчуванням у 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>

і в представленнях ви можете отримати доступ до нього так:

@User.FirstName
@User.LastName

9
Приємна реалізація; стежте за тим, як RoleManagerModule замінює ваш власний головний принцип RolePrincipal. Це викликало у мене багато болю - stackoverflow.com/questions/10742259 / ...
Девід Keaveny

9
ОК, я знайшов рішення, просто додайте ще один перемикач, який проходить "" (порожній рядок), оскільки електронна пошта та ідентифікатор будуть анонімними.
П’єр-Ален Віджеант

3
DateTime.Now.AddMinutes (N) ... як зробити це, щоб він не виходив з користувачем через N хвилин, чи можна ввійти в систему (якщо користувач перевірить "Запам'ятати мене" наприклад)?
1110 р.

4
Якщо ви використовуєте WebApiController, вам необхідно встановити Thread.CurrentPrincipalна Application_PostAuthenticateRequestдля нього , щоб працювати , оскільки він не покладається наHttpContext.Current.User
Джонатан Левісона

3
@AbhinavGujjar FormsAuthentication.SignOut();добре працює для мене.
LukeP

109

Я не можу говорити безпосередньо для ASP.NET MVC, але для веб-форм ASP.NET фокус полягає у створенні FormsAuthenticationTicket та зашифрувати його у файлі cookie після того, як користувач отримав автентифікацію. Таким чином, вам потрібно зателефонувати до бази даних лише один раз (або AD або все, що ви використовуєте для здійснення вашої автентифікації), і кожен наступний запит буде підтверджуватися на основі квитка, який зберігається в файлі cookie.

Хороша стаття про це: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (непрацююча посилання)

Редагувати:

Оскільки посилання вище порушено, я би рекомендував рішення LukeP у своїй відповіді вище: https://stackoverflow.com/a/10524305 - я б також запропонував би прийняту відповідь змінити на відповідну.

Редагувати 2: Альтернатива розірваному посиланню: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html


Починаючи з PHP, я завжди розміщував таку інформацію, як UserID та інші фрагменти, необхідні для надання обмеженого доступу в сесії. Збереження його на стороні клієнта змушує мене нервувати, чи можете ви прокоментувати, чому це не буде проблемою?
Джон Зумбрум

@JohnZ - сам квиток шифрується на сервері, перш ніж він буде надісланий по дроту, тому це не так, як клієнт матиме доступ до даних, що зберігаються в квитку. Зауважте, що ідентифікатори сеансу зберігаються також у файлі cookie, тому насправді все не так вже й відрізняється.
Джон Раш

3
Якщо ви тут, то варто поглянути на рішення
LukeP

2
Я завжди переймався можливістю перевищення максимального розміру файлів cookie ( stackoverflow.com/questions/8706924/… ) при такому підході. Я прагну використовувати Cacheяк Sessionзаміну для збереження даних на сервері. Хтось може мені сказати, чи є це хибним підходом?
Червоний Таз

2
Гарний підхід. Одна з потенційних проблем з цим полягає в тому, що якщо у вашого об’єкта користувача є декілька властивостей (особливо, якщо є будь-які вкладені об'єкти), створення файлу cookie буде вимкнено, коли зашифроване значення перевищить 4 Кб (набагато простіше, ніж ви могли б подумати). Якщо ви зберігаєте лише ключові дані, це добре, але тоді вам доведеться все-таки натиснути БД. Інший розгляд - «оновлення» даних файлів cookie, коли в об’єкта користувача є зміна підпису чи логіки.
Джеффрі Худік

63

Ось приклад, щоб виконати роботу. bool isValid встановлюється, переглядаючи деякий сховище даних (скажімо, вашу базу даних користувачів). UserID - це лише ідентифікатор, який я підтримую. Ви можете додати додаткову інформацію, наприклад електронну адресу, до даних користувача.

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

у асоційованому списку додайте наступний код для отримання інформації

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

Коли ви збираєтеся використовувати інформацію пізніше, ви можете отримати доступ до свого власного основного принципу наступним чином.

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

це дозволить отримати доступ до користувацької інформації користувача.


2
FYI - це Request.Cookies [] (множина)
Dan Esparza

10
Не забудьте встановити Thread.CurrentPrincipal, а також Context.User на CustomPrincipal.
Russ Cam

6
Звідки береться GetUserIdentity ()?
Райан

Як я вже згадував у коментарі, це дає реалізацію System.Web.Security.IIdentity. Google про цей інтерфейс
Sriwantha Attanayake

16

MVC надає вам метод OnAuthorize, який висить у ваших класах контролерів. Або ви можете використовувати спеціальний фільтр дій для здійснення авторизації. MVC робить це досить просто. Я опублікував повідомлення про це тут. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0


Але сеанс може бути втрачено, і користувач все ще підтверджує автентифікацію. Ні ?
Драгуф

@brady gaster, я читаю вашу публікацію в блозі (спасибі!), чому б хтось використав перезапис "OnAuthorize ()", як це було зазначено у вашій публікації над записом global.asax "... AuthenticateRequest (..)", згаданим іншим відповіді? Чи один віддає перевагу над іншим у встановленні принципового користувача?
RayLoveless

10

Ось рішення, якщо вам потрібно підключити деякі способи до @User для використання у ваших поглядах. Немає рішення для будь-якого серйозного налаштування членства, але якщо оригінальне запитання було потрібне лише для переглядів, то, можливо, цього буде достатньо. Нижче було використано для перевірки змінної, повернутої з авторизаційного фільтра, яка використовується для перевірки того, чи є деякі посилання, де ми будемо представлені чи ні (не для будь-якої логіки авторизації чи надання доступу).

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

Потім просто додайте посилання в областях web.config і називайте його, як показано нижче, у поданому вигляді.

@User.IsEditor()

1
У вашому рішенні нам знову потрібно робити дзвінки до бази даних кожного разу. Оскільки об’єкт користувача не має власних властивостей. У ньому є лише Ім'я та IsAuthanticated
oneNiceFriend

Це повністю залежить від вашої реалізації та бажаної поведінки. Мій зразок містить 0 рядків бази даних, або ролі, логіки. Якщо ви користуєтесь IsInRole, він, у свою чергу, може бути кешований у файлі cookie, я вважаю. Або ви реалізуєте власну логіку кешування.
База

3

На основі відповіді LukeP та додайте деякі методи налаштування timeoutта requireSSLспівпраці з нимиWeb.config .

Посилання на посилання

Змінені коди LukeP

1, встановлений timeoutна основі Web.Config. FormsAuthentication.Timeout отримає значення тайм - ауту, який визначений в web.config. Я завершив наступні функції, щоб повернути 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, Налаштуйте файл cookie надійним чи ні, виходячи з RequireSSLконфігурації.

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

3

Гаразд, значить, я серйозний криптовалюта тут, перетягнувши це дуже старе питання, але є набагато простіший підхід до цього, який торкнувся @Baserz вище. А це - використовувати комбінацію методів розширення C # та кешування (НЕ використовувати сеанс).

Фактично, Microsoft вже надала ряд таких розширень у Microsoft.AspNet.Identity.IdentityExtensionsпросторі імен. Наприклад, GetUserId()це метод розширення, який повертає ідентифікатор користувача. Є також GetUserName()іFindFirstValue() , що повертає претензії на основі IPrincipal.

Тому вам потрібно лише включити простір імен, а потім зателефонувати User.Identity.GetUserName() щоб отримати ім'я користувачів, налаштоване за ідентифікацією ASP.NET

Я не впевнений, чи це кешування, оскільки старіший ідентифікатор ASP.NET не відкритий, і я не намагався його інженером змінити. Однак, якщо це не так, ви можете написати власний метод розширення, який буде кешувати цей результат протягом певного часу.


Чому "не використовувати сеанс"?
Олексій

@jitbit - тому що сеанс є ненадійним та небезпечним. З тієї ж причини ви ніколи не використовуєте сеанс з метою безпеки.
Ерік Функенбуш

"Ненадійні" можна вирішити шляхом повторного заселення сеансу (якщо він порожній). "Небезпечно" - є способи захисту від викрадення сеансу (використовуючи лише HTTPS + інші способи). Але я фактично з вами згоден. Де б ви кеширували це тоді? Інформація, як IsUserAdministratorі UserEmailт.д.? Ти думаєш HttpRuntime.Cache?
Алекс

@jitbit - це один варіант, або інше рішення кешування, якщо у вас є. Обов’язково закінчується запис кешу через певний проміжок часу. Небезпечно також стосується локальної системи, оскільки ви можете вручну змінити файл cookie та вгадати ідентифікатори сеансу. Людина в середині - не єдина турбота.
Ерік Функенбуш

2

Як додаток до коду LukeP для користувачів веб-форм (не MVC), якщо ви хочете спростити доступ до коду позаду своїх сторінок, просто додайте код нижче до базової сторінки та отримайте базову сторінку на всіх своїх сторінках:

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

Тож у своєму коді позаду ви можете просто отримати доступ:

User.FirstName or User.LastName

Чого мені не вистачає в сценарії веб-форми, це як отримати таку саму поведінку в коді, не прив’язаному до сторінки, наприклад, в httpmodules, якщо я завжди повинен додавати кастинг у кожному класі чи є розумніший спосіб отримати це?

Дякую за ваші відповіді і подякувати LukeP , так як я використав свої приклади в якості бази для мого призначеного для користувача користувача (який тепер має User.Roles, User.Tasks, User.HasPath(int), User.Settings.Timeoutі багато інших приємні речі)


0

Я спробував рішення, запропоноване LukeP, і виявив, що воно не підтримує атрибут Autize. Отже, я трохи його змінив.

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

І нарешті в 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;
        }
    }

Тепер я можу отримати доступ до даних у представленнях даних та контролерах, просто зателефонувавши

User.ExInfo()

Для виходу я просто дзвоню

AuthManager.SignOut();

де AuthManager

HttpContext.GetOwinContext().Authentication
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.