具有MVC站点的某些要求的细化权限
我不喜欢内置的会员提供商。 我决定自己动手。 我正在努力想出一个在动作级别执行授权的好方法。 以下是我要求的要求:
- 属性用法 – 我喜欢这个,因为它在调用堆栈中控制得非常高,并且是组织权限的好地方。
- 没有神奇的字符串 – 这就是我偏离当前角色提供者的原因。 我不想留下那些不能轻易重命名的字符串。
- 权限应该由另一个权限组成。 示例:
ReadWrite
具有Read
权限。 就像或用enum一样。
注意:有些人认为这套要求过于宽泛(见评论)。 我不这么认为,我认为它们相当简单。
最大的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中,这只是丑陋的)。
此外,如果有一个比动作/控制器上的属性更好的替代方案,那么我全都倾听建议。
编辑:也是从这篇文章 ,我已经读过BitArray
类。 它看起来有点难看,特别是对于数据库中的任意存储。
首先,我要感谢你吮吸我回答这个问题;)
这是一个很长的答案,只是一个起点。 您必须弄清楚如何为用户分配角色以及如何在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
包装一组枚举值,并validationIPrincipal
是否是其中一个的成员:
public class RoleSet { public RoleSet(IEnumerable 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 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 compositeRoles = new Dictionary(); 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 Resolve(Role role) { Role[] roles; if (compositeRoles.TryGetValue(role, out roles) == false) { roles = new[] {role}; } return roles; } }
把它连接起来
我们需要经过身份validation的用户才能使用。 我在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"); } } }
好像你想要一些非常灵活的东西,并且不需要安全检查所要求的东西 。 所以,这取决于“你准备好了多远”。
为了使这种方式成为正确的方向,我强烈建议您查看基于声明的访问控制方面 。 并以本文为起点和ASP.NET MVC示例。
但请记住,这是一个复杂的话题。 非常灵活(甚至允许联合访问控制,没有任何代码更改)但复杂。
我们必须采用这种方式使我们的应用程序完全不可用于那些“正确检查”的实现。 我们所有的系统都知道他们需要执行特定操作的“声明”并根据提供的用户身份(这也是“声明”)请求它。 角色,权限和其他声明可以轻松“翻译”为对我们的应用程序有意义的特定于应用程序的“声明”。 充分灵活。
PS它没有解决“魔术字符串”等技术问题(你必须认为这取决于你的情况),但为你提供了非常灵活的访问控制架构。
所以@thomas似乎有一个很好的答案,但它更多地包含你使用枚举的要求,把它带入IPricipal
会理解的角色。 我的解决方案是自下而上的,所以你可以在我的顶部使用thomas的解决方案来实现IPrincipal
我真的需要类似于你想要的东西,并且总是害怕使用Forms身份validation(是的,你也很害怕,我知道,但是听我说)所以我总是用表单推出自己的廉价身份validation,但很多在我学习mvc的过程中,事情发生了变化(在过去的几周里)。表单auth是非常独立的,非常灵活。 基本上你并没有真正使用表单身份validation,但你只是将自己的逻辑插入系统。
所以这就是我解决这个问题的方法,( 请注意我自己就是学习者 )。
摘要:
- 你将覆盖一些表单auth类来validation你自己的用户,(你甚至可以模拟这个)
- 然后你将创建一个
IIdentity
。 - 你用字符串中的角色列表加载
GenericPrincipal
(我知道,没有魔法字符串……继续阅读)
一旦你完成上述任务,MVC就足以了解你想要的东西! 您现在可以在任何控制器上使用[Authorize(Roles = "Write,Read")]
,MVC几乎可以完成所有操作。 现在没有魔法字符串,你只需要创建一个围绕该属性的包装器。
答案很长
您使用MVC附带的Internet应用程序模板,因此首先您要创建MVC项目,在新对话框中,说您想要一个Internet应用程序 。
检查应用程序时,它将有一个覆盖表单身份validation的主类。 IMembershipService
删除本地MembershipProvider变量__provider_,在此类中,您应该至少在ValidateUser
方法中添加逻辑。 (尝试向一个用户/通行证添加虚假身份validation)另请参阅在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; } } }
我问了一个问题,即上述方法的有效性和正确性如何。
好了,现在你差不多完成了,你可以这样装饰你的控制器: [Authorize(Roles="RoleA,RoleB")]
(稍后更多关于字符串)
这里有一个小问题,如果你用AuthorizeAttribute
装饰你的控制器,并且记录的用户没有特定的权限,而不是默认情况下说“拒绝访问” ,用户将被重定向到登录页面再次登录。 你解决这个问题(我从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期望的字符串的强类型。 有关更多信息,请参阅此文
我计划稍后更新我的应用程序以使用强类型,然后我将更新此答案。
我希望它有所帮助。