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.... Я единственный, кто находит все это очень запутанным?

Если бы кто-то мог сказать мне простое, элегантное и эффективное решение для хранения некоторых дополнительных данных на IIdentity без всего лишнего запаха ... это было бы здорово! Я знаю, что есть аналогичные вопросы по SO, но если ответ на этот вопрос мне нужен, я должен пропустить.

 Simon_Weaver06 февр. 2013 г., 13:29
этот вопрос имеет 36 000 просмотров и много голосов. действительно ли это общее требование - и если да, то нет ли лучшего способа, чем все эти «нестандартные вещи»?
 Razzie28 янв. 2012 г., 17:26
Hi Domi, это комбинация только хранения данных, которые никогда не меняются (например, ID пользователя), или обновления cookie непосредственно после того, как пользователь изменяет данные, которые должны быть немедленно отражены в cookie. Если пользователь делает это, я просто обновляю cookie новыми данными. Но я стараюсь не хранить данные, которые часто меняются.
 niico15 апр. 2017 г., 21:08
@ Simon_Weaver Это ясно показывает, что существует потребность в более простой, более гибкой системе идентификации IMHO.
 broadband12 авг. 2016 г., 18:15
Я согласен с вами, есть много информации, как вы опубликовали:MemberShip..., Principal, Identity. ASP.NET должен сделать это проще, проще и максимально двумя подходами для работы с аутентификацией.
 John10 апр. 2015 г., 13:04
@ Simon_Weaver Известно удостоверение ASP.NET, которое легче поддерживает дополнительную пользовательскую информацию в зашифрованном cookie.

Ответы на вопрос(9)

Решение Вопроса

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

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

interface ICustomPrincipal : IPrincipal
{
    int Id { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
}

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

CustomPrincipalSerializeModel - для сериализации пользовательской информации в поле пользовательских данных в объекте FormsAuthenticationTicket.

public class CustomPrincipalSerializeModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

LogIn метод - настройка куки с пользовательской информацией

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

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

Доступ в 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
 Jonathan Levison16 июл. 2013 г., 21:50
Если вы используете WebApiController, вам нужно установитьThread.CurrentPrincipal вApplication_PostAuthenticateRequest для того, чтобы это работало, поскольку это не полагается наHttpContext.Current.User
 111025 дек. 2012 г., 14:00
DateTime.Now.AddMinutes (N) ... как сделать так, чтобы он не выходил из системы через N минут, может ли вошедший в систему пользователь сохранится (например, когда пользователь выберет «Запомнить меня»)?
 David Keaveny26 июн. 2012 г., 09:29
еализация @Nice; следите за тем, чтобы RoleManagerModule заменил ваш пользовательский принципал на RolePrincipal. Это причинило мне много боли - / Stackoverflow.com вопросы / 10742259 / ...
 Pierre-Alain Vigeant20 нояб. 2012 г., 23:27
ok Я нашел решение, просто добавьте еще параметр, который передаст "" (пустая строка) в качестве электронного письма, и идентификатор будет анонимным.
 LukeP29 янв. 2014 г., 22:30
@ AbhinavGujjarFormsAuthentication.SignOut(); у меня отлично работает.

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

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

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

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

 Alex08 июн. 2017 г., 21:31
Почему "не использовать сессию"?
 Alex08 июн. 2017 г., 21:58
«Ненадежный» можно устранить путем повторного заполнения сеанса (если он пуст). «Небезопасный» - есть способы защиты от перехвата сеанса (используя только HTTPS + другие способы). Но я на самом деле согласен с вами. Где бы вы кешировали это тогда? Информация какIsUserAdministrator илиUserEmail так далее.? Вы думаетеHttpRuntime.Cache?
 Erik Funkenbusch08 июн. 2017 г., 21:42
@ jitbit - потому что сессия ненадежна и небезопасна. По той же причине вы никогда не должны использовать сессию в целях безопасности.
 Erik Funkenbusch08 июн. 2017 г., 22:11
@ jitbit - это один из вариантов или другое решение для кэширования, если оно у вас есть. Убедитесь, что истек срок действия записи в кеше. Небезопасный также относится к локальной системе, так как вы можете вручную изменить cookie и угадать идентификаторы сессии. Человек посередине - не единственная забота.

чтобы сделать работу. 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

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

 Dan Esparza20 мая 2010 г., 06:27
FYI - это Request.Cookies [] (множественное число)
 Russ Cam31 мая 2010 г., 11:07
Не забудьте установить Thread.CurrentPrincipal, а также Context.User в CustomPrincipal.
 Sriwantha Attanayake29 янв. 2012 г., 16:05
Как я уже упоминал в комментарии, он дает реализацию System.Web.Security.IIdentity. Google об этом интерфейсе
 Ryan12 мар. 2011 г., 08:03
Откуда берется GetUserIdentity ()?

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

 RayLoveless11 апр. 2016 г., 20:01
@ brady gaster, я прочитал ваше сообщение в блоге (спасибо!), зачем кому-то использовать переопределение «OnAuthorize ()», как упомянуто в вашем сообщении над записью global.asax «... AuthenticateRequest (..)», упомянутой другие ответы? Один предпочтительнее другого в настройке основного пользователя?
 Dragouf18 авг. 2011 г., 14:46
But сессия может быть потеряна, и пользователь все еще аутентифицируется. Нет?

если вам нужно подключить некоторые методы к @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()
 Base05 июн. 2016 г., 16:03
Это полностью зависит от вашей реализации и желаемого поведения. Мой образец содержит 0 строк базы данных, или роль, логику. Я полагаю, что если кто-то использует IsInRole, он может быть кэширован в cookie. Или вы реализуете свою собственную логику кеширования.
 oneNiceFriend05 июн. 2016 г., 10:36
В вашем решении нам снова нужно каждый раз выполнять вызовы из базы данных. Потому что пользовательский объект не имеет пользовательских свойств. Он имеет только Имя и IsAuthanticated

LukeP ответ, и добавьте несколько методов для настройкиtimeout а такжеrequireSSL сотрудничал сWeb.config.

Ссылки ссылки MSDN, объяснение: проверка подлинности с помощью форм в ASP.NET 2.0MSDN, класс FormsAuthentication SO, значение «timeout» проверки подлинности .net Access Forms в коде Модифицированные коды 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, Настройте куки, чтобы быть безопасным или нет, на основеRequireSSL конфигурация.

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

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

Хорошая статья об этом:http: //www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.htm (неработающей ссылке

Редактировать

Так как ссылка выше не работает, я бы порекомендовал решение LukeP в его ответе выше:https: //stackoverflow.com/a/1052430 - Я бы также предложил изменить принятый ответ на этот.

Ред. 2: Альтернатива для неработающей ссылки:https: //web.archive.org/web/20120422011422/http: //ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.htm

 John Zumbrum24 апр. 2012 г., 14:54
Исходя из PHP, я всегда помещал информацию, такую как UserID и другие части, необходимые для предоставления ограниченного доступа в Session. Хранение этого на стороне клиента заставляет меня нервничать, можете ли вы прокомментировать, почему это не будет проблемой?
 Red Taz08 мая 2013 г., 17:14
Я всегда был обеспокоен возможностью превышения максимального размера файлов cookie / Stackoverflow.com вопросы / 8706924 / ...) с таким подходом. Я склонен использоватьCache какSession замена, чтобы сохранить данные на сервере. Может кто-нибудь сказать мне, если это ошибочный подход?
 Geoffrey Hudik05 июн. 2013 г., 03:36
Хороший подход. Одна потенциальная проблема, связанная с этим, заключается в том, что если ваш пользовательский объект имеет более чем несколько свойств (и особенно если есть какие-либо вложенные объекты), создание файла cookie завершится неудачно, как только зашифрованное значение превысит 4 КБ (гораздо проще получить доступ, чем вы думаете). Если вы храните только ключевые данные, это нормально, но тогда вам все равно придется нажимать на БД. Еще одним соображением является «обновление» данных cookie, когда объект пользователя имеет сигнатуру или логические изменения.
 John Rasch24 апр. 2012 г., 16:42
@ JohnZ - сам билет зашифровывается на сервере перед отправкой по проводной связи, поэтому клиент не будет иметь доступа к данным, хранящимся в билете. Обратите внимание, что идентификаторы сессий также хранятся в cookie, поэтому на самом деле они не такие уж и разные.
 mynkow26 апр. 2013 г., 09:59
Если ты здесь, посмотри на решение LukeP

если вы хотите упростить доступ к коду за вашими страницами, просто добавьте приведенный ниже код на базовую страницу и получите базовую страницу на всех ваших страницах:

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 и много других приятных вещей)

предложенное 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

Ваш ответ на вопрос