MVC自定义身份validation,授权和角色实现

请耐心等待我提供详细信息…

我有一个MVC站点,使用FormsAuthentication和自定义服务类进行身份validation,授权,角色/成员资格等。

认证

登录有三种方式: (1)电子邮件+别名(2)OpenID ,以及(3)用户名+密码 。 这三个人都获得了一个auth cookie并开始一个会话。 前两个是访问者使用(仅限会话),第三个是作者/ admin使用数据库帐户。

 public class BaseFormsAuthenticationService : IAuthenticationService { // Disperse auth cookie and store user session info. public virtual void SignIn(UserBase user, bool persistentCookie) { var vmUser = new UserSessionInfoViewModel { Email = user.Email, Name = user.Name, Url = user.Url, Gravatar = user.Gravatar }; if(user.GetType() == typeof(User)) { // roles go into view model as string not enum, see Roles enum below. var rolesInt = ((User)user).Roles; var rolesEnum = (Roles)rolesInt; var rolesString = rolesEnum.ToString(); var rolesStringList = rolesString.Split(',').Select(role => role.Trim()).ToList(); vmUser.Roles = rolesStringList; } // i was serializing the user data and stuffing it in the auth cookie // but I'm simply going to use the Session[] items collection now, so // just ignore this variable and its inclusion in the cookie below. var userData = ""; var ticket = new FormsAuthenticationTicket(1, user.Email, DateTime.UtcNow, DateTime.UtcNow.AddMinutes(30), false, userData, FormsAuthentication.FormsCookiePath); var encryptedTicket = FormsAuthentication.Encrypt(ticket); var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) { HttpOnly = true }; HttpContext.Current.Response.Cookies.Add(authCookie); HttpContext.Current.Session["user"] = vmUser; } } 

角色

权限的简单标志枚举:

 [Flags] public enum Roles { Guest = 0, Editor = 1, Author = 2, Administrator = 4 } 

枚举扩展来帮助枚举标志枚举(哇!)。

 public static class EnumExtensions { private static void IsEnumWithFlags() { if (!typeof(T).IsEnum) throw new ArgumentException(string.Format("Type '{0}' is not an enum", typeof (T).FullName)); if (!Attribute.IsDefined(typeof(T), typeof(FlagsAttribute))) throw new ArgumentException(string.Format("Type '{0}' doesn't have the 'Flags' attribute", typeof(T).FullName)); } public static IEnumerable GetFlags(this T value) where T : struct { IsEnumWithFlags(); return from flag in Enum.GetValues(typeof(T)).Cast() let lValue = Convert.ToInt64(value) let lFlag = Convert.ToInt64(flag) where (lValue & lFlag) != 0 select flag; } } 

授权

Service提供了检查已认证用户角色的方法。

 public class AuthorizationService : IAuthorizationService { // Convert role strings into a Roles enum flags using the additive "|" (OR) operand. public Roles AggregateRoles(IEnumerable roles) { return roles.Aggregate(Roles.Guest, (current, role) => current | (Roles)Enum.Parse(typeof(Roles), role)); } // Checks if a user's roles contains Administrator role. public bool IsAdministrator(Roles userRoles) { return userRoles.HasFlag(Roles.Administrator); } // Checks if user has ANY of the allowed role flags. public bool IsUserInAnyRoles(Roles userRoles, Roles allowedRoles) { var flags = allowedRoles.GetFlags(); return flags.Any(flag => userRoles.HasFlag(flag)); } // Checks if user has ALL required role flags. public bool IsUserInAllRoles(Roles userRoles, Roles requiredRoles) { return ((userRoles & requiredRoles) == requiredRoles); } // Validate authorization public bool IsAuthorized(UserSessionInfoViewModel user, Roles roles) { // convert comma delimited roles to enum flags, and check privileges. var userRoles = AggregateRoles(user.Roles); return IsAdministrator(userRoles) || IsUserInAnyRoles(userRoles, roles); } } 

我选择通过属性在我的控制器中使用它:

 public class AuthorizationFilter : IAuthorizationFilter { private readonly IAuthorizationService _authorizationService; private readonly Roles _authorizedRoles; ///  /// Constructor ///  /// The AuthorizedRolesAttribute is used on actions and designates the /// required roles. Using dependency injection we inject the service, as well /// as the attribute's constructor argument (Roles). public AuthorizationFilter(IAuthorizationService authorizationService, Roles authorizedRoles) { _authorizationService = authorizationService; _authorizedRoles = authorizedRoles; } ///  /// Uses injected authorization service to determine if the session user /// has necessary role privileges. ///  /// As authorization code runs at the action level, after the /// caching module, our authorization code is hooked into the caching /// mechanics, to ensure unauthorized users are not served up a /// prior-authorized page. /// Note: Special thanks to TheCloudlessSky on StackOverflow. ///  public void OnAuthorization(AuthorizationContext filterContext) { // User must be authenticated and Session not be null if (!filterContext.HttpContext.User.Identity.IsAuthenticated || filterContext.HttpContext.Session == null) HandleUnauthorizedRequest(filterContext); else { // if authorized, handle cache validation if (_authorizationService.IsAuthorized((UserSessionInfoViewModel)filterContext.HttpContext.Session["user"], _authorizedRoles)) { var cache = filterContext.HttpContext.Response.Cache; cache.SetProxyMaxAge(new TimeSpan(0)); cache.AddValidationCallback((HttpContext context, object o, ref HttpValidationStatus status) => AuthorizeCache(context), null); } else HandleUnauthorizedRequest(filterContext); } } 

我使用这个属性在我的控制器中装饰Actions,就像微软的[Authorize]没有参数意味着让任何经过身份validation的人(对我而言,它是Enum = 0,没有必需的角色)。

关于包装背景信息(phew)…并写出所有这些我回答了我的第一个问题。 在这一点上,我很好奇我的设置的适当性:

  1. 我是否需要手动获取auth cookie并填充HttpContext的FormsIdentity主体,还是应该是自动的?

  2. 在属性/filterOnAuthorization()检查身份validation的任何问题?

  3. 使用Session[]存储我的视图模型与在auth cookie中序列化它有什么权衡?

  4. 这个解决方案是否似乎足够好地遵循“关注点分离”的理想? (奖金因为它是更加舆论导向的问题)

我的CodeReview答案中的交叉post:

我会抓住你回答你的问题并提出一些建议:

  1. 如果您在web.config配置了FormsAuthentication,它将自动为您提取cookie,因此您不必对FormsIdentity进行任何手动填充。 无论如何,这很容易测试。

  2. 您可能希望覆盖AuthorizeCoreOnAuthorization以获得有效的授权属性。 AuthorizeCore方法返回一个布尔值,用于确定用户是否可以访问给定资源。 OnAuthorization不会返回,通常用于根据身份validation状态触发其他内容。

  3. 我认为session-vs-cookie问题在很大程度上是偏好,但我建议你参加会议有几个原因。 最大的原因是cookie随着每个请求传输,而现在你可能只有一点点数据,随着时间的推移,谁知道你在那里的东西。 添加加密开销,它可能会变得足够大以减慢请求。 将它存储在会话中也会将数据的所有权交到您手中(而不是将其放在客户手中并依赖于您解密和使用它)。 我要提出的一个建议是将该会话访问包装在静态UserContext类中,类似于HttpContext ,因此您可以像UserContext.Current.UserData一样进行调用。 请参阅下面的示例代码。

  4. 我无法真正谈论它是否是一个很好的分离关注点,但对我来说它看起来是一个很好的解决方案。 这与我见过的其他MVC身份validation方法没有什么不同。 事实上,我在我的应用程序中使用了非常相似的东西。

最后一个问题 – 为什么要手动构建和设置FormsAuthentication cookie而不是使用FormsAuthentication.SetAuthCookie ? 只是好奇。

静态上下文类的示例代码

 public class UserContext { private UserContext() { } public static UserContext Current { get { if (HttpContext.Current == null || HttpContext.Current.Session == null) return null; if (HttpContext.Current.Session["UserContext"] == null) BuildUserContext(); return (UserContext)HttpContext.Current.Session["UserContext"]; } } private static void BuildUserContext() { BuildUserContext(HttpContext.Current.User); } private static void BuildUserContext(IPrincipal user) { if (!user.Identity.IsAuthenticated) return; // For my application, I use DI to get a service to retrieve my domain // user by the IPrincipal var personService = DependencyResolver.Current.GetService(); var person = personService.FindBy(user); if (person == null) return; var uc = new UserContext { IsAuthenticated = true }; // Here is where you would populate the user data (in my case a SiteUser object) var siteUser = new SiteUser(); // This is a call to ValueInjecter, but you could map the properties however // you wanted. You might even be able to put your object in there if it's a POCO siteUser.InjectFrom(person); // Next, stick the user data into the context uc.SiteUser = siteUser; // Finally, save it into your session HttpContext.Current.Session["UserContext"] = uc; } #region Class members public bool IsAuthenticated { get; internal set; } public SiteUser SiteUser { get; internal set; } // I have this method to allow me to pull my domain object from the context. // I can't store the domain object itself because I'm using NHibernate and // its proxy setup breaks this sort of thing public UserBase GetDomainUser() { var svc = DependencyResolver.Current.GetService(); return svc.FindBy(ActiveSiteUser.Id); } // I have these for some user-switching operations I support public void Refresh() { BuildUserContext(); } public void Flush() { HttpContext.Current.Session["UserContext"] = null; } #endregion } 

在过去,我已经将属性直接放在UserContext类上以访问我需要的用户数据,但是当我将其用于其他更复杂的项目时,我决定将其移动到SiteUser类:

 public class SiteUser { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get { return FirstName + " " + LastName; } } public string AvatarUrl { get; set; } public int TimezoneUtcOffset { get; set; } // Any other data I need... } 

虽然我觉得你做得很好,但我怀疑你为什么重新创造这个轮子。 由于microsoft为此提供了一个系统,称为成员和角色提供者。 为什么不编写自定义成员资格和角色提供程序,那么您不必创建自己的身份validation属性和/或filter,只需使用内置的属性和/或filter。

您的MVC自定义身份validation,授权和角色实现看起来很不错。 要回答您的第一个问题,当您不使用memberhipprovider时,您必须自己填充FormsIdentity主体。 我使用的解决方案在这里描述我的博客