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

559

Мне нужно сделать что-то довольно простое: в моем приложении ASP.NET MVC я хочу установить пользовательский параметр IIdentity/IPrincipal. Какое бы ни было удобнее и удобнее. Я хочу расширить значение по умолчанию, чтобы я мог называть что-то вроде User.Identity.Id и User.Identity.Role. Ничего необычного, просто некоторые дополнительные свойства.

Я прочитал множество статей и вопросов, но мне кажется, что я делаю это сложнее, чем на самом деле. Я думал, что это будет легко. Если пользователь входит в систему, я хочу установить пользовательский параметр IIdentity. Поэтому я подумал, что буду реализовывать Application_PostAuthenticateRequest в своем global.asax. Тем не менее, это вызвано каждым запросом, и я не хочу делать вызов в базу данных по каждому запросу, который запрашивал бы все данные из базы данных и помещал бы пользовательский объект IPrincipal. Это также кажется очень ненужным, медленным и не в том месте (там используются вызовы базы данных), но я могу ошибаться. Или откуда еще взялись эти данные?

Поэтому я подумал, что всякий раз, когда пользователь входит в систему, я могу добавить некоторые необходимые переменные в свой сеанс, которые я добавляю к пользовательскому идентификатору Identity в обработчике событий Application_PostAuthenticateRequest. Тем не менее, мой Context.Session здесь null, так что это тоже не путь.

Я работаю над этим в течение дня, и я чувствую, что что-то не хватает. Это не должно быть слишком сложно сделать, правильно? Я также немного смущен всем (полу) связанным с этим материалом. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Я единственный, кто считает все это очень запутанным?

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

  • 0
    как вы интегрировали это с заранее написанным кодом MVC для входа?
  • 0
    Стефан, вам не нужно много рисковать в отношении существующего AccountController. Хитрость заключается в том, чтобы установить cookie в global.asax, и вам нужно только самостоятельно записать некоторые данные в cookie для проверки подлинности с помощью форм после входа в AccountController. Для этого вы можете использовать FormsAuthenticationTicket, который вы можете передавать пользовательские данные.
Показать ещё 7 комментариев
Теги:
asp.net-mvc
forms-authentication
iprincipal
iidentity

8 ответов

790
Лучший ответ

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

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

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

    if (Membership.ValidateUser(viewModel.Email, viewModel.Password))
    {
        var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First();
    
        CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel();
        serializeModel.Id = user.Id;
        serializeModel.FirstName = user.FirstName;
        serializeModel.LastName = user.LastName;
    
        JavaScriptSerializer serializer = new JavaScriptSerializer();
    
        string userData = serializer.Serialize(serializeModel);
    
        FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                 1,
                 viewModel.Email,
                 DateTime.Now,
                 DateTime.Now.AddMinutes(15),
                 false,
                 userData);
    
        string encTicket = FormsAuthentication.Encrypt(authTicket);
        HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
        Response.Cookies.Add(faCookie);
    
        return RedirectToAction("Index", "Home");
    }
    
  • 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

    @((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
  • 8
    Хорошая реализация; следите за тем, чтобы RoleManagerModule заменил ваш пользовательский принципал на RolePrincipal. Это доставило мне много боли - stackoverflow.com/questions/10742259/…
  • 0
    Прекрасно, у меня только один вопрос: может ли сохранение идентификатора пользователя в билете авторизации форм, как вы только что описали, вызвать какие-либо проблемы с безопасностью? Возможно ли, что пользователь может подделать эти данные?
Показать ещё 49 комментариев
109

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

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

Edit:

Так как ссылка выше нарушена, я бы порекомендовал решение LukeP в своем ответе выше: https://stackoverflow.com/questions/1064271/asp-net-mvc-set-custom-iidentity-or-iprincipal - Я бы также предположил, что принятый ответ будет изменен на этот один.

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

  • 0
    Исходя из PHP, я всегда помещал информацию, такую как UserID и другие части, необходимые для предоставления ограниченного доступа в Session. Хранение его на стороне клиента заставляет меня нервничать, можете ли вы прокомментировать, почему это не будет проблемой?
  • 0
    @JohnZ - сам билет зашифровывается на сервере перед отправкой по проводам, поэтому клиент не будет иметь доступа к данным, хранящимся в билете. Обратите внимание, что идентификаторы сессий также хранятся в cookie-файлах, поэтому они не так уж и отличаются.
Показать ещё 6 комментариев
60

Вот пример, чтобы выполнить работу. 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.
Показать ещё 2 комментария
15

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

  • 0
    Но сессия может быть потеряна, и пользователь все равно аутентифицируется. Нет?
  • 0
    @brady gaster, я прочитал ваше сообщение в блоге (спасибо!), зачем кому-то использовать переопределение «OnAuthorize ()», как упомянуто в вашем сообщении, над записью global.asax «... AuthenticateRequest (..)», упомянутой другим ответы? Является ли одно предпочтение перед другим при настройке основного пользователя?
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()
  • 0
    В вашем решении нам снова нужно делать вызовы базы данных каждый раз. Потому что пользовательский объект не имеет пользовательских свойств. Он имеет только имя и IsAuthanticated
  • 0
    Это полностью зависит от вашей реализации и желаемого поведения. Мой образец содержит 0 строк базы данных, или роль, логику. Я полагаю, что если кто-то использует IsInRole, он может быть кэширован в cookie. Или вы реализуете свою собственную логику кеширования.
3

На основе ответа LukeP и добавьте некоторые методы настройки timeout и requireSSL, сотрудничающие с Web.config.

Ссылки ссылок

Измененные коды LukeP

1, установите timeout на основе Web.config. FormsAuthentication.Timeout получит значение тайм-аута, которое определено в web.config. Я завернул следующие функции, которые возвращают a 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;
2

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

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

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

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

  • 0
    Почему "не использовать сессию"?
  • 0
    @jitbit - потому что сессия ненадежна и небезопасна. По той же причине вы никогда не должны использовать сессию в целях безопасности.
Показать ещё 2 комментария
2

В качестве дополнения к LukeP-коду для пользователей веб-форм (а не MVC), если вы хотите упростить доступ в коде позади ваших страниц, просто добавьте код ниже на базовую страницу и выведите базовую страницу на всех ваших страницах

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

Итак, в вашем коде позади вы можете просто получить доступ:

User.FirstName or User.LastName

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню