ASP.NET MVC - установить пользовательский IIdentity или IPrincipal

Мне нужно сделать что-то довольно простое: в моем приложении 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 без всего лишнего запаха ... это было бы здорово! Я знаю, что есть аналогичные вопросы по SO, но если ответ мне нужен, я должен был пропустить.

Привет, Domi, это комбинация только хранения данных, которые никогда не меняются (например, ID пользователя), или обновления cookie непосредственно после того, как пользователь изменяет данные, которые должны быть немедленно отражены в cookie. Если пользователь делает это, я просто обновляю cookie новыми данными. Но я стараюсь не хранить данные, которые часто меняются.
Razzie 28.01.2012 16:26:06
Этот вопрос имеет 36 тысяч просмотров и много голосов. действительно ли это общее требование - и если да, то нет ли лучшего способа, чем все эти «нестандартные вещи»?
Simon_Weaver 6.02.2013 12:29:54
@Simon_Weaver Известно удостоверение ASP.NET, которое легче поддерживает дополнительную пользовательскую информацию в зашифрованном cookie.
John 10.04.2015 11:04:18
Я согласен с вами, что нужно много информации , как вы писали: MemberShip..., Principal, Identity. ASP.NET должен упростить, упростить и максимально использовать два подхода к аутентификации.
broadband 12.08.2016 16:15:56
@Simon_Weaver Это ясно показывает, что существует спрос на более простую, более гибкую систему идентификации IMHO.
niico 15.04.2017 19:08:04
9 ОТВЕТОВ
РЕШЕНИЕ

Вот как я это делаю.

Я решил использовать IPrincipal вместо IIdentity, потому что это означает, что мне не нужно реализовывать как IIdentity, так и IPrincipal.

  1. Создать интерфейс

    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 - для сериализации настраиваемой информации в поле пользовательских данных в объекте FormsAuthenticationTicket.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  4. Метод входа в систему - настройка куки с пользовательской информацией

    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 Views

    @((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
835
31.05.2017 17:36:41
Хорошая реализация; не упустите RoleManagerModule, заменив свой пользовательский принципал на RolePrincipal. Это
David Keaveny 26.06.2012 07:29:26
Хорошо, я нашел решение, просто добавьте еще параметр, который передает "" (пустая строка) в качестве электронного письма, и Идентификация будет анонимной.
Pierre-Alain Vigeant 20.11.2012 22:27:10
DateTime.Now.AddMinutes (N) ... как сделать так, чтобы он не выходил из системы через N минут, может ли вошедший в систему пользователь сохранится (например, когда пользователь выберет «Запомнить меня»)?
1110 25.12.2012 13:00:31
Если вы используете WebApiController, вам необходимо установить Thread.CurrentPrincipalна Application_PostAuthenticateRequestдля него , чтобы работать , поскольку он не полагается наHttpContext.Current.User
Jonathan Levison 16.07.2013 19:50:42
@AbhinavGujjar FormsAuthentication.SignOut();прекрасно работает для меня.
LukeP 29.01.2014 21:30:16

Я не могу говорить напрямую о ASP.NET MVC, но для ASP.NET Web Forms хитрость заключается в том, чтобы создать 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

109
23.05.2017 12:10:41
Исходя из PHP, я всегда помещал информацию, такую ​​как UserID и другие части, необходимые для предоставления ограниченного доступа в Session. Хранение этого на стороне клиента заставляет меня нервничать, можете ли вы прокомментировать, почему это не будет проблемой?
John Zumbrum 24.04.2012 12:54:31
@JohnZ - сам билет зашифровывается на сервере перед отправкой по сети, поэтому клиент не будет иметь доступа к данным, хранящимся в билете. Обратите внимание, что идентификаторы сессий также хранятся в cookie, так что на самом деле они не так уж отличаются.
John Rasch 24.04.2012 14:42:48
Если вы здесь, вы должны посмотреть на решение
mynkow 26.04.2013 07:59:19
Я всегда был обеспокоен возможностью превышения максимального размера cookie ( stackoverflow.com/questions/8706924/… ) при таком подходе. Я склонен использовать в Cacheкачестве Sessionзамены, чтобы сохранить данные на сервере. Может кто-нибудь сказать мне, если это ошибочный подход?
Red Taz 8.05.2013 15:14:45
Хороший подход. Одна потенциальная проблема, связанная с этим, заключается в том, что если ваш пользовательский объект имеет более чем несколько свойств (и особенно, если есть какие-либо вложенные объекты), создание файла cookie завершится неудачно, как только зашифрованное значение превысит 4 КБ (гораздо проще получить доступ, чем вы думаете). Если вы храните только ключевые данные, это нормально, но тогда вам все равно придется нажимать на БД. Еще одним соображением является «обновление» данных cookie, когда объект пользователя имеет сигнатуру или логические изменения.
Geoffrey Hudik 5.06.2013 01:36:35

Вот пример, чтобы сделать работу. 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);
     }
}

в golbal asax добавьте следующий код, чтобы получить вашу информацию

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

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

63
6.10.2014 19:35:43
К вашему сведению - это Request.Cookies [] (множественное число)
Dan Esparza 20.05.2010 04:27:40
Не забудьте установить Thread.CurrentPrincipal, а также Context.User в CustomPrincipal.
Russ Cam 31.05.2010 09:07:43
Откуда берется GetUserIdentity ()?
Ryan 12.03.2011 07:03:13
Как я уже упоминал в комментарии, он дает реализацию System.Web.Security.IIdentity. Google об этом интерфейсе
Sriwantha Attanayake 29.01.2012 15:05:55

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

16
7.05.2014 23:37:54
Но сессия может быть потеряна, и пользователь все равно аутентифицируется. Нет?
Dragouf 18.08.2011 12:46:32
@brady gaster, я прочитал ваше сообщение в блоге (спасибо!), почему кто-то использовал переопределение «OnAuthorize ()», как упомянуто в вашем сообщении, над записью global.asax «... AuthenticateRequest (..)», упомянутой другим ответы? Является ли одно предпочтение перед другим при настройке основного пользователя?
RayLoveless 11.04.2016 18:01:26

В качестве дополнения к коду 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

Чего мне не хватает в сценарии веб-формы, так это как получить такое же поведение в коде, не привязанном к странице, например, в модулях http, следует ли мне всегда добавлять приведение в каждый класс или есть более разумный способ получить это?

Спасибо за ваши ответы и поблагодарить LukeP , так как я использовал свои примеры в качестве базы для моего пользовательского пользователя (который теперь имеет User.Roles, User.Tasks, User.HasPath(int), User.Settings.Timeoutи многие другие приятные вещи)

2
25.07.2013 13:56:15

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

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()
10
9.03.2013 23:10:04
В вашем решении нам снова нужно делать вызовы базы данных каждый раз. Потому что пользовательский объект не имеет пользовательских свойств. У него только Имя и IsAuthanticated
oneNiceFriend 5.06.2016 08:36:36
Это полностью зависит от вашей реализации и желаемого поведения. Мой образец содержит 0 строк базы данных, или роль, логику. Я полагаю, что если кто-то использует IsInRole, он может быть кэширован в cookie. Или вы реализуете свою собственную логику кеширования.
Base 5.06.2016 14:03:42

Основываясь на ответе 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
23.05.2017 12:18:25

Хорошо, поэтому я серьезный криптограф, перетаскивая этот очень старый вопрос, но есть гораздо более простой подход к этому, который был затронут @Baserz выше. И это использовать комбинацию методов C # Extension и кэширования (НЕ использовать сессию).

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

Таким образом, вам нужно только включить пространство имен, а затем вызвать, User.Identity.GetUserName()чтобы получить имя пользователя в соответствии с настройками ASP.NET Identity.

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

3
31.05.2017 20:49:11
Почему "не использовать сессию"?
Alex 8.06.2017 19:31:32
@jitbit - потому что сеанс ненадежен и небезопасен. По той же причине вы никогда не должны использовать сессию в целях безопасности.
Erik Funkenbusch 8.06.2017 19:42:16
«Ненадежность» может быть устранена путем повторного заполнения сеанса (если он пуст). «Небезопасный» - есть способы защиты от перехвата сеанса (используя только HTTPS + другие способы). Но я на самом деле согласен с вами. Где бы вы кешировали это тогда? Информация нравится IsUserAdministratorили UserEmailт. Д.? Ты думаешь HttpRuntime.Cache?
Alex 8.06.2017 19:58:59
@jitbit - это один из вариантов или другое решение для кэширования, если оно у вас есть. Убедитесь, что истек срок действия записи в кеше. Небезопасный также относится к локальной системе, так как вы можете вручную изменить cookie и угадать идентификаторы сессии. Человек посередине - не единственная забота.
Erik Funkenbusch 8.06.2017 20:11:56

Я попробовал решение, предложенное LukeP, и обнаружил, что оно не поддерживает атрибут Authorize. Итак, я немного изменил его.

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
0
26.06.2018 09:02:53