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, но, если мне нужен ответ, я должен был упустить это из виду.
10 ответов
Вот как я это делаю.
Я решил использовать 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; } }
Метод входа в систему - настройка куки с пользовательской информацией
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
Я не могу говорить напрямую о ASP.NET MVC, но для ASP.NET Web Forms хитрость заключается в создании FormsAuthenticationTicket
и зашифровать его в файл cookie после проверки подлинности пользователя. Таким образом, вам нужно будет вызвать базу данных только один раз (или AD, или что-то еще, что вы используете для выполнения вашей аутентификации), и каждый последующий запрос будет аутентифицироваться на основе тикета, хранящегося в cookie.
Хорошая статья на эту тему: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (неработающая ссылка)
Редактировать:
Так как ссылка выше не работает, я бы порекомендовал решение LukeP в его ответе выше: /questions/8247303/aspnet-mvc-ustanovit-polzovatelskij-iidentity-ili-iprincipal/8247340#8247340 - Я бы также предложил изменить принятый ответ на этот.
Изменить 2: Альтернатива для неработающей ссылки: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html
Вот пример, чтобы сделать работу. 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
это позволит вам получить доступ к пользовательской информации пользователя.
MVC предоставляет вам метод OnAuthorize, который зависает от классов вашего контроллера. Или вы можете использовать фильтр пользовательских действий для выполнения авторизации. MVC делает это довольно легко сделать. Я разместил пост в блоге об этом здесь. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0
Вот решение, если вам нужно подключить некоторые методы к @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()
Основываясь на ответе LukeP, добавьте несколько методов для настройки timeout
а также requireSSL
сотрудничал с Web.config
,
Ссылки ссылки
- Объяснение MSDN: Аутентификация с помощью форм в ASP.NET 2.0
- MSDN, класс Forms Authentication
- 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. Настройте cookie-файлы как безопасные или нет, основываясь на RequireSSL
конфигурации.
HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
// respect to `RequreSSL` in `Web.Config`
bool bSSL = FormsAuthentication.RequireSSL;
faCookie.Secure = bSSL;
Итак, я серьезный хранитель криптовалют, перетаскивая этот очень старый вопрос, но есть гораздо более простой подход к этому, который был затронут @Baserz выше. И это использовать комбинацию методов C# Extension и кэширования (НЕ использовать сессию).
На самом деле, Microsoft уже предоставила ряд таких расширений в Microsoft.AspNet.Identity.IdentityExtensions
Пространство имен. Например, GetUserId()
это метод расширения, который возвращает идентификатор пользователя. Существует также GetUserName()
а также FindFirstValue()
, который возвращает претензии на основе IPrincipal.
Таким образом, вам нужно только включить пространство имен, а затем вызвать User.Identity.GetUserName()
чтобы получить имя пользователя в соответствии с настройками ASP.NET Identity.
Я не уверен, что это кэшируется, так как более старая идентификация ASP.NET не с открытым исходным кодом, и я не удосужился его перепроектировать. Однако, если это не так, вы можете написать свой собственный метод расширения, который будет кэшировать этот результат в течение определенного периода времени.
В качестве дополнения к коду 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
и много других приятных мелочей)
Я попробовал решение, предложенное 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
за
/Web.config
:
добавьте следующий код под
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" />
</authentication>