MVC自定义身份验证,授权和角色实施 [英] MVC Custom Authentication, Authorization, and Roles Implementation

查看:368
本文介绍了MVC自定义身份验证,授权和角色实施的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我熊,因为我提供的问题的详细信息...

我有一个MVC的网站,使用 FormsAuthentication 和定制服务类的认证,授权,角色/会员等。

验证

有三种方法可以登录:(1)电子邮件+别名(2)的OpenID (3)用户名+密码。所有这三个得到了用户的身份验证cookie并启动一个会话。前两个是游客(仅限于会议),第三为作者/管理与DB账户中使用。

 公共类BaseFormsAuthenticationService:IAuthenticationService
{
    //分散AUTH的cookie和存储用户会话信息。
    公共虚拟无效签到(用户群的用户,布尔persistentCookie)
    {
        VAR vmUser =新UserSessionInfoViewModel {电子邮件= user.Email,名称= user.Name,URL = user.Url,的Gravatar = user.Gravatar};        如果(user.GetType()== typeof运算(用户)){
            //角色进入视图模型作为字符串不枚举,请参阅下文角色枚举。
            VAR rolesInt =((用户)用户).Roles;
            VAR rolesEnum =(角色)rolesInt;
            变种rolesString = rolesEnum.ToString();
            变种rolesStringList = rolesString.Split(,)选择(角色= GT; role.Trim())。了ToList();
            vmUser.Roles = rolesStringList;
        }        //我是序列化的用户数据,并在AUTH饼干塞进它
        //但我只是现在要使用Session []项目收集,从而使
        //忽略这个变量和它下面的cookie的包容性。
        VAR用户数据=;        VAR票=新的FormsAuthenticationTicket(1,user.Email,DateTime.UtcNow,DateTime.UtcNow.AddMinutes(30),假的​​,用户数据,FormsAuthentication.FormsCookiePath);
        VAR的encryptedTicket = FormsAuthentication.Encrypt(票);
        VAR authCookie =新的HttpCookie(FormsAuthentication.FormsCookieName,encryptedTicket中){仅Http = TRUE};
        HttpContext.Current.Response.Cookies.Add(authCookie);
        HttpContext.Current.Session [用户] = vmUser;
    }
}

角色

一个简单的标志枚举的权限:

  [国旗]
公共枚举角色
{
    客户= 0,
    编辑= 1,
    作者= 2,
    管理员= 4
}

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

 公共静态类EnumExtensions
{
    私有静态无效IsEnumWithFlags< T>()
    {
        如果(!typeof运算(T).IsEnum)
            抛出新的ArgumentException(的String.Format(类型{0}不是枚举的typeof(T).FullName));
        如果(!Attribute.IsDefined(typeof运算(T)的typeof(的FlagsAttribute)))
            抛出新的ArgumentException(的String.Format(类型{0}不具有标志属性中的typeof(T).FullName));
    }    公共静态的IEnumerable< T> GetFlags< T>(这件T值),其中T:结构
    {
        IsEnumWithFlags< T>();
        从标志Enum.GetValues​​(typeof运算(T))演员LT返回; T>()让左值= Convert.ToInt64(值)让lFlag = Convert.ToInt64(标志),其中(左值和放大器; lFlag)!= 0选择标志;
    }
}

授权

服务提供方法,用于检查验证用户的角色。

 公共类AuthorizationService:IAuthorizationService
{
    //使用添加剂角色转换成字符串一个角色枚举标志| (OR)操作数。
    公共角色AggregateRoles(IEnumerable的<串GT;的角色)
    {
        返回roles.Aggregate(Roles.Guest,(电流,角色)=>当前|(角色)Enum.Parse(typeof运算(角色)角色));
    }    //检查某个用户的角色包括管理员角色。
    公共BOOL IsAdministrator(角色的UserRole)
    {
        返回userRoles.HasFlag(Roles.Administrator);
    }    //如果用户有任何允许角色的标志检查。
    公共BOOL IsUserInAnyRoles(角色的UserRole,角色allowedRoles)
    {
        变种国旗= allowedRoles.GetFlags();
        返回flags.Any(标志=> userRoles.HasFlag(标志));
    }    //如果用户拥有所有必需的角色标志检查。
    公共BOOL IsUserInAllRoles(角色的UserRole,角色requiredRoles)
    {
        返回((&的UserRole安培; requiredRoles)== requiredRoles);
    }    //验证授权
    公共BOOL IsAuthorized(UserSessionInfoViewModel用户,角色角色)
    {
        //转换逗号分隔的角色枚举标志,检查权限。
        VAR =的UserRole AggregateRoles(user.Roles);
        返回IsAdministrator(的UserRole)|| IsUserInAnyRoles(的UserRole,角色);
    }
}

我选择了通过一个属性在我的控制器使用这样的:

 公共类AuthorizationFilter:个IAuthorizationFilter
{
    私人只读IAuthorizationService _authorizationService;
    私人只读角色_authorizedRoles;    ///<总结>
    ///构造
    ///< /总结>
    ///<说明>在AuthorizedRolesAttribute上使用的行为,并指定了
    ///必需的角色。使用依赖注入我们注入的服务,以及
    ///为属性的构造函数的参数(角色)LT; /备注>
    公共AuthorizationFilter(IAuthorizationService authorizationService,角色au​​thorizedRoles)
    {
        _authorizationService = authorizationService;
        _authorizedRoles = authorizedRoles;
    }    ///<总结>
    ///用途注入授权服务,以确定该会话的用户
    ///有必要的角色权限。
    ///< /总结>
    ///<说明>如授权code。在操作层面上运行,后
    ///缓存模块,我们的授权code挂入高速缓存
    ///机制,以确保未经授权的用户都未能送达的,
    ///事先授权的页面。
    ///注:特别感谢TheCloudlessSky在计算器上。
    ///< /言论>
    公共无效OnAuthorization(AuthorizationContext filterContext)
    {
        //用户必须通过身份验证和会话不能为null
        如果(!filterContext.HttpContext.User.Identity.IsAuthenticated || filterContext.HttpContext.Session == NULL)
            HandleUnauthorizedRequest(filterContext);
        其他{
            //如果授权,处理缓存验证
            如果(_authorizationService.IsAuthorized((UserSessionInfoViewModel)filterContext.HttpContext.Session[\"user\"], _authorizedRoles)){
                VAR缓存= filterContext.HttpContext.Response.Cache;
                cache.SetProxyMaxAge(新的TimeSpan(0));
                cache.AddValidationCallback((HttpContext的背景下,对象o,楼盘HttpValidationStatus状态)=> AuthorizeCache(上下文),NULL);
            }
            其他
                HandleUnauthorizedRequest(filterContext);
        }
    }

我装点在我的控制器操作与此属性,而像微软的 [授权] 没有PARAMS手段让任何人认证(对我来说是枚举= 0,无需角色)。

这是关于包装了背景信息(呼)...写这一切了,我回答我的第一个问题。在这一点上我很好奇我的设置是否恰当:


  1. 我是否需要手工抽丝身份验证cookie并填充FormsIdentity主要为的HttpContext 或者应该说是全自动的吗?


  2. 与属性/过滤器内检查认证的任何问题 OnAuthorization()


  3. 什么是权衡使用会议[] 来存储我的视图模型主场迎战身份验证cookie中序列化呢?


  4. 这是否解决方案似乎遵循理想的关注分离不够好? (奖金,因为它是更加面向舆论的问题)



解决方案

交叉后hre​​f=\"http://$c$creview.stackexchange.com/a/7286/9510\">我的codeReview回答

我会在回答你的问题采取刺伤,并提供一些建议:


  1. 如果您已经在FormsAuthentication 的web.config ,它会自动将你的饼干,所以你不应该做任何手动配置人口该FormsIdentity。这是pretty容易在任何情况下进行测试。


  2. 您可能要同时重写 AuthorizeCore OnAuthorization 一个有效的授权属性。在 AuthorizeCore 方法返回一个布尔值,用于确定用户是否有权访问给定资源。在 OnAuthorization 不返回,通常采用基于认证状态触发其他的东西。


  3. 我觉得会话VS-cookie的问题在很大程度上是preference,但我建议你用几个原因会去。最大的原因是该cookie与每个请求发送,而现在你可能只有在它的数据一点点,随着时间的推移谁知道你会在那里的东西。添加加密开销,它可能获得足够大的请求放缓。在会话中存储它也让数据的所有权在你的手中(而不是把它在客户的手中,依靠你解密和使用它)。一个建议我想提出的是包装的会话访问在静态的UserContext 类,类似于的HttpContext ,所以你可以只是让像 UserContext.Current.UserData 的电话。见下面例如code。


  4. 我真的不能给它是否是关注一个很好的分离说话,但它看起来像一个很好的解决方案给我。这不是不像我见过的其他MVC认证方法。我用我其实非常的应用程序类似的东西。


最后一个问题 - 你为什么要建立和设置FormsAuthentication饼干手动而不是使用的 FormsAuthentication.SetAuthCookie ?只是好奇。

示例code静态上下文类

 公共类的UserContext
{
    私人的UserContext()
    {
    }    公共静态电流的UserContext
    {
        得到
        {
            如果(HttpContext.Current == NULL || HttpContext.Current.Session == NULL)
                返回null;            如果(HttpContext.Current.Session [的UserContext] == NULL)
                BuildUserContext();            返回(的UserContext)HttpContext.Current.Session [的UserContext];
        }
    }    私有静态无效BuildUserContext()
    {
        BuildUserContext(HttpContext.Current.User);
    }    私有静态无效BuildUserContext(用户的IPrincipal)
    {
        如果回报(user.Identity.IsAuthenticated!);        //我的应用程序,我用DI得到一个服务来获取我的域名
        //用户通过的IPrincipal
        VAR personService = DependencyResolver.Current.GetService< IUserBaseService>();
        变种人= personService.FindBy(用户);        如果(人== NULL)回报;        VAR UC =新的UserContext {IsAuthenticated =真};        //在这里你会填充用户数据(在我的情况下,SiteUser对象)
        VAR siteUser =新SiteUser();
        //这是ValueInjecter一个电话,但你可以在属性映射然而,
        // 你自找的。你甚至可以把你的目标在那里,如果它是一个POCO
        siteUser.InjectFrom< FlatLoopValueInjection>(人);        //接下来,黏住用户数据到上下文
        uc.SiteUser = siteUser;        //最后,将其保存到您的会话
        HttpContext.Current.Session [的UserContext] = UC;
    }
    #区域类成员
    公共BOOL IsAuthenticated {搞定;内部设置; }
    公共SiteUser SiteUser {搞定;内部设置; }    //我有这样的方法,让我从上下文中拉我的域对象。
    //因为我使用NHibernate的,我不能存储域对象本身
    //其代理设置打破了这样的事情
    公共用户群GetDomainUser()
    {
        VAR SVC = DependencyResolver.Current.GetService< IUserBaseService>();
        返回svc.FindBy(ActiveSiteUser.Id);
    }    //我有这些对我支持一些用户切换操作
    公共无效刷新()
    {
        BuildUserContext();
    }    公共无效的flush()
    {
        HttpContext.Current.Session [的UserContext] = NULL;
    }
    #endregion
}

在过去,我已经把物业直接放在的UserContext 类访问我所需要的用户数据,但我用这个其他更复杂的项目,我决定把它移动到 SiteUser 类:

 公共类SiteUser
{
    公众诠释标识{搞定;组; }
    公共字符串名字{获得;组; }
    公共字符串名字{获得;组; }
    公共字符串全名
    {
        {返回名字++姓氏; }
    }
    公共字符串AvatarUrl {搞定;组; }    公众诠释TimezoneUtcOffset {搞定;组; }    //我需要的任何其他数据...
}

Bear with me as I provide details for the issue...

I've got an MVC site, using FormsAuthentication and custom service classes for Authentication, Authorization, Roles/Membership, etc.

Authentication

There are three ways to sign-on: (1) Email + Alias, (2) OpenID, and (3) Username + Password. All three get the user an auth cookie and start a session. The first two are used by visitors (session only) and the third for authors/admin with db accounts.

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;
    }
}

Roles

A simple flags enum for permissions:

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

Enum extension to help enumerate flag enums (wow!).

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;
    }
}

Authorization

Service offers methods for checking an authenticated user's roles.

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);
    }
}

I chose to use this in my controllers via an attribute:

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);             
        }
    }

I decorate Actions in my Controllers with this attribute, and like Microsoft's [Authorize] no params means let in anyone authenticated (for me it is Enum = 0, no required roles).

That about wraps up the background info (phew)... and writing all this out I answered my first question. At this point I am curious about the appropriateness of my setup:

  1. Do I need to manually snag the auth cookie and populate the FormsIdentity principal for the HttpContext or should that be automatic?

  2. Any issues with checking authentication within the attribute/filter OnAuthorization()?

  3. What are tradeoffs in using Session[] to store my view model vs. serializing it within the auth cookie?

  4. Does this solution seem to follow the 'separation of concerns' ideals well enough? (Bonus as it is more opinion-oriented question)

解决方案

Cross-post from my CodeReview answer:

I'll take a stab at answering your questions and provide some suggestions:

  1. If you have FormsAuthentication configured in web.config, it will automatically pull the cookie for you, so you shouldn't have to do any manual population of the FormsIdentity. This is pretty easy to test in any case.

  2. You probably want to override both AuthorizeCore and OnAuthorization for an effective authorization attribute. The AuthorizeCore method returns a boolean and is used to determine whether the user has access to a given resource. The OnAuthorization doesn't return and is generally used to trigger other things based on the authentication status.

  3. I think the session-vs-cookie question is largely preference, but I'd recommend going with the session for a few reasons. The biggest reason is that the cookie is transmitted with every request, and while right now you may only have a little bit of data in it, as time progresses who knows what you'll stuff in there. Add encryption overhead and it could get large enough to slow down requests. Storing it in the session also puts ownership of the data in your hands (versus putting it in the client's hands and relying on you to decrypt and use it). One suggestion I would make is wrapping that session access up in a static UserContext class, similar to HttpContext, so you could just make a call like UserContext.Current.UserData. See below for example code.

  4. I can't really speak to whether it is a good separation of concerns, but it looks like a good solution to me. It's not unlike other MVC authentication approaches I've seen. I'm using something very similar in my apps in fact.

One last question -- why did you build and set the FormsAuthentication cookie manually instead of using FormsAuthentication.SetAuthCookie? Just curious.

Example code for static context class

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
}

In the past I had put properties directly on the UserContext class for accessing the user data I needed, but as I've used this for other, more complicated projects, I decided to move it to a SiteUser class:

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自定义身份验证,授权和角色实施的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆