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

Razzie
источник
1
Привет, Domi, это комбинация только хранения данных, которые никогда не меняются (например, идентификатора пользователя), или обновления cookie непосредственно после того, как пользователь изменяет данные, которые должны быть немедленно отражены в cookie. Если пользователь делает это, я просто обновляю cookie новыми данными. Но я стараюсь не хранить данные, которые часто меняются.
Раззи
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. 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
LukeP
источник
9
Хорошая реализация; следите за тем, чтобы RoleManagerModule заменил ваш пользовательский принципал на RolePrincipal. Это
доставило
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 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

Джон Раш
источник
Исходя из PHP, я всегда помещал информацию, такую ​​как UserID и другие части, необходимые для предоставления ограниченного доступа в Session. Хранение этого на стороне клиента заставляет меня нервничать, можете ли вы прокомментировать, почему это не будет проблемой?
Джон Зумбрум
@JohnZ - сам билет зашифровывается на сервере перед отправкой по сети, поэтому клиент не будет иметь доступа к данным, хранящимся в билете. Обратите внимание, что идентификаторы сессий также хранятся в cookie-файлах, поэтому они не так уж и отличаются.
Джон Раш
3
Если вы здесь, вы должны посмотреть на решение
LukeP
2
Я всегда был обеспокоен возможностью превышения максимального размера cookie ( stackoverflow.com/questions/8706924/… ) при таком подходе. Я склонен использовать в Cacheкачестве Sessionзамены, чтобы сохранить данные на сервере. Может кто-нибудь сказать мне, если это ошибочный подход?
Red Taz
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);
     }
}

в 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

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

Шриванта Аттанаяке
источник
2
К вашему сведению - это Request.Cookies [] (множественное число)
Дэн Эспарза
10
Не забудьте установить Thread.CurrentPrincipal, а также Context.User в CustomPrincipal.
Расс Кэм
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 для использования в ваших представлениях. Нет решения для какой-либо серьезной настройки членства, но если исходный вопрос был необходим только для одних мнений, то, возможно, этого было бы достаточно. Нижеследующее использовалось для проверки переменной, возвращенной из 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()
База
источник
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;
AechoLiu
источник
3

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

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

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

Я не уверен, что это кэшируется, так как более старая идентификация 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

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

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

Manight
источник
0

Я попробовал решение, предложенное 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
Василий Иванов
источник