Вопрос по asp.net-mvc, asp.net – ASP.NET MVC - установить пользовательский IIdentity или IPrincipal

624

Мне нужно сделать что-то довольно простое: в моем приложении 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, но если ответ на этот вопрос мне нужен, я должен пропустить.

этот вопрос имеет 36 000 просмотров и много голосов. действительно ли это общее требование - и если да, то нет ли лучшего способа, чем все эти «нестандартные вещи»? Simon_Weaver
Hi Domi, это комбинация только хранения данных, которые никогда не меняются (например, ID пользователя), или обновления cookie непосредственно после того, как пользователь изменяет данные, которые должны быть немедленно отражены в cookie. Если пользователь делает это, я просто обновляю cookie новыми данными. Но я стараюсь не хранить данные, которые часто меняются. Razzie
@ Simon_Weaver Это ясно показывает, что существует потребность в более простой, более гибкой системе идентификации IMHO. niico
@ Simon_Weaver Известно удостоверение ASP.NET, которое легче поддерживает дополнительную пользовательскую информацию в зашифрованном cookie. John
Я согласен с вами, есть много информации, как вы опубликовали:MemberShip..., Principal, Identity. ASP.NET должен сделать это проще, проще и максимально двумя подходами для работы с аутентификацией. broadband

Ваш Ответ

9   ответов
812

Я решил использовать 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
еализация @Nice; следите за тем, чтобы RoleManagerModule заменил ваш пользовательский принципал на RolePrincipal. Это причинило мне много боли - / Stackoverflow.com вопросы / 10742259 / ... David Keaveny
ok Я нашел решение, просто добавьте еще параметр, который передаст "" (пустая строка) в качестве электронного письма, и идентификатор будет анонимным. Pierre-Alain Vigeant
DateTime.Now.AddMinutes (N) ... как сделать так, чтобы он не выходил из системы через N минут, может ли вошедший в систему пользователь сохранится (например, когда пользователь выберет «Запомнить меня»)? 1110
Если вы используете WebApiController, вам нужно установитьThread.CurrentPrincipal вApplication_PostAuthenticateRequest для того, чтобы это работало, поскольку это не полагается наHttpContext.Current.User Jonathan Levison
@ AbhinavGujjarFormsAuthentication.SignOut(); у меня отлично работает. LukeP
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

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

FYI - это Request.Cookies [] (множественное число) Dan Esparza
Не забудьте установить Thread.CurrentPrincipal, а также Context.User в CustomPrincipal. Russ Cam
Откуда берется GetUserIdentity ()? Ryan
Как я уже упоминал в комментарии, он дает реализацию System.Web.Security.IIdentity. Google об этом интерфейсе Sriwantha Attanayake
15

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

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

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

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

но для 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

Исходя из PHP, я всегда помещал информацию, такую как UserID и другие части, необходимые для предоставления ограниченного доступа в Session. Хранение этого на стороне клиента заставляет меня нервничать, можете ли вы прокомментировать, почему это не будет проблемой? John Zumbrum
@ JohnZ - сам билет зашифровывается на сервере перед отправкой по проводной связи, поэтому клиент не будет иметь доступа к данным, хранящимся в билете. Обратите внимание, что идентификаторы сессий также хранятся в cookie, поэтому на самом деле они не такие уж и разные. John Rasch
Если ты здесь, посмотри на решение LukeP mynkow
Я всегда был обеспокоен возможностью превышения максимального размера файлов cookie / Stackoverflow.com вопросы / 8706924 / ...) с таким подходом. Я склонен использоватьCache какSession замена, чтобы сохранить данные на сервере. Может кто-нибудь сказать мне, если это ошибочный подход? Red Taz
Хороший подход. Одна потенциальная проблема, связанная с этим, заключается в том, что если ваш пользовательский объект имеет более чем несколько свойств (и особенно если есть какие-либо вложенные объекты), создание файла cookie завершится неудачно, как только зашифрованное значение превысит 4 КБ (гораздо проще получить доступ, чем вы думаете). Если вы храните только ключевые данные, это нормально, но тогда вам все равно придется нажимать на БД. Еще одним соображением является «обновление» данных cookie, когда объект пользователя имеет сигнатуру или логические изменения. Geoffrey Hudik
3

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

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

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

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

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

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

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, и обнаружил, что оно не поддерживает атрибут 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

Похожие вопросы