MVC自定义身份验证、授权和角色实现

本文关键字:角色 实现 授权 自定义 身份验证 MVC | 更新日期: 2023-09-27 18:18:32

请耐心听我解释…

我有一个MVC网站,使用FormsAuthentication和自定义服务类的身份验证,授权,角色/成员资格等。

认证

有三种登录方式:(1) Email + Alias(2) OpenID(3) Username + Password。这三种方法都为用户获取一个验证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);             
        }
    }

我用这个属性在我的控制器中装饰Actions,像微软的[Authorize]一样,没有参数意味着允许任何经过身份验证的人进入(对我来说它是Enum = 0,不需要角色)。

这大概包含了背景信息(唷)…写下这些,我回答了我的第一个问题。在这一点上,我很好奇我的设置是否合适:

  1. 我是否需要手动抓取验证cookie并填充HttpContext的FormsIdentity主体,或者应该是自动的?

  2. 检查属性/过滤器OnAuthorization()中的身份验证是否有问题?

  3. 使用Session[]来存储我的视图模型与在验证cookie内序列化它的权衡是什么?

  4. 这个解决方案是否足够好地遵循了"关注点分离"的理想?

MVC自定义身份验证、授权和角色实现

我的CodeReview答案:

我将试着回答你的问题并提供一些建议:

  1. 如果你在web.config中配置了FormsAuthentication,它会自动为你拉出cookie,所以你不应该做任何手工填充FormsIdentity。

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

  3. 我认为会话vs cookie的问题很大程度上是个人偏好,但我建议使用会话有几个原因。最大的原因是每个请求都会传输cookie,虽然现在你可能只有一点点数据在里面,但随着时间的推移,谁知道你会在里面放什么。如果增加加密开销,它可能会变得足够大,从而降低请求速度。将其存储在会话中也将数据的所有权置于您的手中(而不是将其置于客户端手中并依赖您解密和使用它)。我提出的一个建议是将会话访问包装在一个静态UserContext类中,类似于HttpContext,这样您就可以像UserContext.Current.UserData一样进行调用。示例代码如下:

  4. 我真的不能说这是否是一个很好的关注点分离,但它看起来对我来说是一个很好的解决方案。它与我见过的其他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类访问我需要的用户数据,但因为我已经使用它的其他,更复杂的项目,我决定把它移动到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...
}

虽然我认为你做得很好,但我质疑你为什么要重新创建轮子。因为微软为此提供了一个系统,叫做会员和角色提供者。为什么不编写一个自定义成员和角色提供程序呢?这样您就不必创建自己的授权属性和/或过滤器,只需使用内置的属性和/或过滤器即可。

您的MVC自定义身份验证、授权和角色实现看起来不错。要回答第一个问题,当您不使用成员关系提供程序时,您必须自己填充FormsIdentity主体。我使用的一个解决方案描述在这里我的博客