请耐心等待我提供有关此问题的详细信息......
我有一个MVC网站,使用FormsAuthentication
和自定义服务类进行身份验证,授权,角色/成员资格等。
有三种登录方式:(1)电子邮件+别名,(2)OpenID 和(3)用户名+密码。这三个人都获得了一个auth cookie并开始一个会话。前两个由访问者使用(仅限会话),第三个用于作者/管理员使用数据库帐户。
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<T>()
{
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<T> GetFlags<T>(this T value) where T : struct
{
IsEnumWithFlags<T>();
return from flag in Enum.GetValues(typeof(T)).Cast<T>() let lValue = Convert.ToInt64(value) let lFlag = Convert.ToInt64(flag) where (lValue & lFlag) != 0 select flag;
}
}
服务提供了检查经过身份验证的用户角色的方法。
public class AuthorizationService : IAuthorizationService
{
// Convert role strings into a Roles enum flags using the additive "|" (OR) operand.
public Roles AggregateRoles(IEnumerable<string> 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;
/// <summary>
/// Constructor
/// </summary>
/// <remarks>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).</remarks>
public AuthorizationFilter(IAuthorizationService authorizationService, Roles authorizedRoles)
{
_authorizationService = authorizationService;
_authorizedRoles = authorizedRoles;
}
/// <summary>
/// Uses injected authorization service to determine if the session user
/// has necessary role privileges.
/// </summary>
/// <remarks>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.
/// </remarks>
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);
}
}
我在我的控制器中用这个属性装饰了动作,就像微软的[Authorize]
没有参数意味着允许任何经过身份验证的人(对我而言,它是Enum = 0,没有必需的角色)。
关于包装背景信息(phew)...并写出所有这些我回答了我的第一个问题。在这一点上,我很好奇我的设置的适当性:
我是否需要手动获取auth cookie并填充HttpContext
的FormsIdentity主体,还是应该是自动的?
在属性/过滤器OnAuthorization()
中检查身份验证的任何问题?
使用Session[]
存储我的视图模型与在auth cookie中序列化它有什么权衡?
此解决方案是否似乎遵循了关注点的分离&#39;理想得足够好? (奖金因为它是更多以意见为导向的问题)
答案 0 :(得分:8)
来自my CodeReview answer的交叉发布:
我会回答你的问题并提出一些建议:
如果您在web.config
中配置了FormsAuthentication,它将自动为您提取cookie,因此您不必对FormsIdentity进行任何手动填充。无论如何,这很容易测试。
您可能希望覆盖AuthorizeCore
和OnAuthorization
以获得有效的授权属性。 AuthorizeCore
方法返回布尔值,用于确定用户是否可以访问给定资源。 OnAuthorization
不会返回,通常用于根据身份验证状态触发其他内容。
我认为session-vs-cookie问题主要是偏好,但我建议你继续使用会话,原因有几个。最大的原因是cookie随着每个请求传输,而现在你可能只有一点点数据,随着时间的推移,谁知道你会在那里做什么。添加加密开销,它可能会变得足够大以减慢请求。将它存储在会话中也会将数据的所有权交到您的手中(而不是将其放在客户手中并依赖于您解密和使用它)。我要提出的一个建议是将该会话访问包含在静态UserContext
类中,类似于HttpContext
,这样您就可以像UserContext.Current.UserData
那样进行调用。请参阅下面的示例代码。
我无法真正说出这是一个很好的分离问题,但对我来说这似乎是一个很好的解决方案。这与我见过的其他MVC身份验证方法没有什么不同。事实上,我在我的应用程序中使用的东西非常相似。
最后一个问题 - 为什么要手动构建和设置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<IUserBaseService>();
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<FlatLoopValueInjection>(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<IUserBaseService>();
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
类上以访问我需要的用户数据,但是当我将其用于其他更复杂的项目时,我决定将其移动到{{ 1}} class:
SiteUser
答案 1 :(得分:7)
虽然我认为你做得很好,但我怀疑你为什么重新创造这个轮子。由于microsoft为此提供了一个系统,称为成员和角色提供者。为什么不编写自定义成员资格和角色提供程序,那么您不必创建自己的身份验证属性和/或过滤器,只需使用内置的。
答案 2 :(得分:1)
您的MVC自定义身份验证,授权和角色实施看起来很不错。要回答您的第一个问题,当您不使用memberhipprovider时,您必须自己填充FormsIdentity主体。我使用的解决方案在这里描述My Blog