具有MVC站点的某些要求的细化权限

时间:2010-12-05 16:01:49

标签: c# .net asp.net-mvc permissions

我不喜欢内置的会员提供商。我决定自己推出。我正试图想出一个在动作级别执行授权的好方法。以下是我尝试的要求:

  1. 属性用法 - 我喜欢这个,因为它在调用堆栈中控制得非常高,是一个组织权限的好地方。
  2. 没有神奇的字符串 - 这就是我偏离当前角色提供者的原因。我不想留下无法轻易重命名的字符串。
  3. 权限应该由一个其他权限组成。示例:ReadWrite拥有Read的权限。就像或用enum一样。
  4. 注意:有些人认为这套要求过于宽泛(见评论)。我不这么认为,我认为它们相当简单。

    最大的showstopper是属性使用。只能有“常量表达式,typeof表达式或属性参数类型的数组创建表达式”。

    我在考虑使用这样的东西来使操作具有静态访问权限。在属性内部,它会将int“转换”为实际的Permission或其他......:

    public static class Operations
    {
        public static class SectionA
        {
            public const int Read = 1;
            public const int ReadWrite = 2;
        }
    
        public static class SectionB
        {
            // ... and so on...
        }
    }
    

    但它确实限制了构图。我相信你在想“你为什么不去enum路线?”好吧,我想计划改变的事情,并且不想限制为32(int)或64(long)操作,并且必须稍后进行大量重写(同样在db中,这只是丑陋的)。

    此外,如果有一个比动作/控制器上的属性更好的替代方案,那么我全神贯注于建议。

    编辑:同样来自this post,我读过有关BitArray课程的内容。似乎有点丑陋,特别是对于数据库中的任意存储。

3 个答案:

答案 0 :(得分:5)

首先,我要感谢你吮吸我回答这个问题;)

这是一个很长的答案,只是一个起点。您必须弄清楚如何为用户分配角色以及如何在AuthenticateRequest中重新创建角色。

如果这不能回答你的问题,我希望它会成为一种灵感。享受!

装饰控制器操作

我开始在默认的HomeController中装饰这两个动作:

    [AuthorizeRoles(Role.Read)]
    public ActionResult Index()
    {
        ViewData["Message"] = "Welcome to ASP.NET MVC!";

        return View();
    }

    [AuthorizeRoles(Role.Write)]
    public ActionResult About()
    {
        return View();
    }

然后,应授予ReadWrite角色中的所有用户访问权限。我在这里选择使用枚举作为魔术字符串的类型安全占位符。这个枚举的作用只不过是占位符。没有复合枚举值,必须在其他地方维护。稍后会详细介绍。

public enum Role
{
    Read,
    Write,
    ReadWrite
}

实施新的授权属性

由于字符串不见了,我需要一个新的authorize属性:

public class AuthorizeRolesAttribute : AuthorizeAttribute
{
    private readonly RoleSet authorizedRoles;

    public AuthorizeRolesAttribute(params Role[] roles)
    {
        authorizedRoles = new RoleSet(roles);
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        return authorizedRoles.Includes(httpContext.User);
    }
}

RoleSet包含一组枚举值,并验证IPrincipal是否是其中一个的成员:

public class RoleSet
{
    public RoleSet(IEnumerable<Role> roles)
    {
        Names = roles.Select(role => role.ToString());
    }

    public bool Includes(IPrincipal user)
    {
        return Names.Any(user.IsInRole);
    }

    public bool Includes(string role)
    {
        return Names.Contains(role);
    }

    public IEnumerable<string> Names { get; private set; }
}

维护角色

CompositeRoleSet是注册和处理复合角色的地方。 CreateDefault()是所有复合材料注册的地方。 Resolve()将获取角色列表(枚举值)并将复合转换为单个对应项。

public class CompositeRoleSet
{
    public static CompositeRoleSet CreateDefault()
    {
        var set = new CompositeRoleSet();
        set.Register(Role.ReadWrite, Role.Read, Role.Write);
        return set;
    }

    private readonly Dictionary<Role, Role[]> compositeRoles = new Dictionary<Role, Role[]>();

    private void Register(Role composite, params Role[] contains)
    {
        compositeRoles.Add(composite, contains);
    }

    public RoleSet Resolve(params Role[] roles)
    {
        return new RoleSet(roles.SelectMany(Resolve));
    }

    private IEnumerable<Role> Resolve(Role role)
    {
        Role[] roles;
        if (compositeRoles.TryGetValue(role, out roles) == false)
        {
            roles = new[] {role};
        }

        return roles;
    }
}

布线

我们需要经过身份验证的用户才能使用。我在global.asax中欺骗并硬编码:

    public MvcApplication()
    {
        AuthenticateRequest += OnAuthenticateRequest;
    }

    private void OnAuthenticateRequest(object sender, EventArgs eventArgs)
    {
        var allRoles = CompositeRoleSet.CreateDefault();
        var roles = allRoles.Resolve(Role.ReadWrite);
        Context.User = new ApplicationUser(roles);
    }

最后,我们需要一个IPrincipal来理解这一切:

public class ApplicationUser : IPrincipal
{
    private readonly RoleSet roles;

    public ApplicationUser(RoleSet roles)
    {
        this.roles = roles;
    }

    public bool IsInRole(string role)
    {
        return roles.Includes(role);
    }

    public IIdentity Identity
    {
        get { return new GenericIdentity("User"); }
    }
}

答案 1 :(得分:0)

似乎你想要一些非常灵活的东西,并且不需要可以要求的东西来进行安全检查。所以,这取决于“你准备好了多远”。

为了使这种方式成为正确的方向,我强烈建议您查看基于声明的访问控制一侧。并以 this 文章为起点和ASP.NET MVC示例。

但请记住,这是一个复杂主题。非常灵活(甚至允许Federated Access Control没有任何代码更改)但很复杂。

我们必须采用这种方式让我们的应用程序完全无法使用那些“正确检查”的实现。我们所有的系统都知道他们需要执行某些操作的“声明”,并根据提供的用户身份(这也是“声明”)要求它。角色,权限和其他声明可以轻松“翻译”为对我们的应用程序有意义的特定于应用程序的“声明”。充分的灵活性。

P.S。它并没有解决“魔术字符串”等技术问题(您必须根据自己的情况来考虑),但为您提供了非常灵活的访问控制架构。

答案 2 :(得分:0)

所以@thomas似乎有一个很好的答案,但它更多地包含你使用枚举的要求,把它带入IPricipal将理解的角色。我的解决方案是自下而上,所以你可以使用thomas&#39;我的解决方案实现IPrincipal

我真的需要类似于你想要的东西,并且总是害怕表单身份验证,(是的,你也害怕,我知道,但是听我说)所以我总是用表单推出自己的廉价身份验证但是,当我学习mvc(过去几周)时,很多事情发生了变化。表单auth非常独立,而且非常灵活。基本上你并没有真正使用表单身份验证,而只是将你自己的逻辑插入系统。

所以这就是我解决这个问题的方法,(要注意我自己是一个学习者)。

要点:

  1. 你将覆盖一些表单auth类来验证你自己的用户,(你甚至可以嘲笑这个)
  2. 然后你要创建IIdentity
  3. 你加载GenericPrincipal字符串中的角色列表(我知道,没有魔法字符串......继续阅读)
  4. 一旦你完成上述工作,MVC就足以了解你想要的东西!您现在可以在任何控制器上使用[Authorize(Roles = "Write,Read")],MVC几乎可以执行所有操作。现在没有魔法字符串,你只需要创建一个围绕该属性的包装器。


    答案很长

    您使用MVC附带的Internet应用程序模板,首先,您首先要创建MVC项目,在新对话框中,假设您需要 Internet Application

    检查应用程序时,它将有一个覆盖表单身份验证的主类。 IMembershipService删除本地MembershipProvider变量__provider_,在此类中,您应该至少将逻辑添加到ValidateUser方法中。 (尝试向一个用户/通行证添加虚假身份验证)另请参阅在VS.

    中创建的默认v测试应用程序

    实施IIdentity

        public class MyIdentity : IIdentity
        {
                public MyIdentity(string username)
                {
                   _username = username;//auth from the DB here.
                   //load up the Roles from db or whatever
                }
                string _username;
                public User UserData { get; set; }
                #region IIdentity Members
                public string AuthenticationType
                {
                    get { return "MyOwn.Authentication"; }
                }
                public bool IsAuthenticated
                {
                    get { return true; }
                }
                public string Name
                {
                    get { return _username; }
                }
                #endregion
                public string[] Roles
                {
                    get
                    {
                        return //get a list of roles as strings from your Db or something.
                    }
                }
        }
    

    请记住,我们仍在使用MVC项目附带的默认Internet应用程序模板。

    所以现在AccountController.LogOn()应如下所示:

     [HttpPost]
     public virtual ActionResult LogOn(LogOnModel model, string returnUrl)
     {
        if (ModelState.IsValid)
        {
           if (MembershipService.ValidateUser(model.UserName, model.Password))
           {
              FormsService.SignIn(model.UserName, model.RememberMe);
              FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(model.UserName, model.RememberMe, 15);
              string encTicket = FormsAuthentication.Encrypt(ticket);
              this.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
    
                        if (Url.IsLocalUrl(returnUrl))
                        {
                            return Redirect(returnUrl);
                        }
                        else
                        {
                            return RedirectToAction("Index", "Home");
                        }
                    }
                    else
                    {
                        ModelState.AddModelError("", "The user name or password provided is incorrect.");
                    }
                }
    

    所以你正在做的是设置一个类似会话的表单票据然后我们会在每个请求上读取它:将它放在Global.asax.cs

    public override void Init()
          {
             this.PostAuthenticateRequest += new EventHandler(MvcApplication_PostAuthenticateRequest);
             base.Init();
          }
    
    void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
          {
              HttpCookie authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
              if (authCookie != null)
              {
                  string encTicket = authCookie.Value;
                     if (!String.IsNullOrEmpty(encTicket))
                     {
                            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(encTicket);
                            MyIdentity id = new MyIdentity(ticket.Name);
                            //HERE is where the magic happens!!
                            GenericPrincipal prin = new GenericPrincipal(id, id.Roles);
                            HttpContext.Current.User = prin;
                     }
               }
          }
    

    我问了一个问题,即上述方法的有效性和正确性是here

    好了,现在你差不多完成了,你可以这样装饰你的控制器: [Authorize(Roles="RoleA,RoleB")](稍后会详细介绍字符串)

    这里有一个小问题,如果你用AuthorizeAttribute装饰你的控制器,并且记录的用户没有特定的权限,而不是说&#34;访问被拒绝&#34; 默认情况下,用户将被重定向到登录页面以再次登录。你解决这个问题(我从SO回答中调整了这个):

    public class RoleAuthorizeAttribute : AuthorizeAttribute
        {
            protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
            {
                // Returns HTTP 401 
                // If user is not logged in prompt
                if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
                {
                    base.HandleUnauthorizedRequest(filterContext);
                }
                // Otherwise deny access
                else
                {
                    filterContext.Result = new RedirectToRouteResult(@"Default", new RouteValueDictionary{
                    {"controller","Account"},
                    {"action","NotAuthorized"}
                    });
                }
            }
        }
    

    现在你要做的就是在AuthorizeAttribute周围添加另一个包装器,以支持将转换为Principal期望的字符串的强类型。 See this article for more

    我计划稍后更新我的应用程序以使用强类型,然后我会更新此答案。

    我希望它有所帮助。