ASP.NET 系列:RBAC权限设计
权限系统的组成通常包括RBAC模型、权限验证、权限管理以及界面访问控制。现有的一些权限系统分析通常存在以下问题:
(1)没有权限的设计思路
认为所有系统都可以使用一套基于Table设计的权限系统。事实上设计权限系统的重点是判断角色的稳定性和找出最小授权需求。角色的稳定性决定了系统是通过角色判断权限还是需要引入RBAC方式,最小授权需求防止我们过度设计导致超出授权需求的权限粒度。
(2)没有独立的RBAC模型的概念
直接使用实体类表示RBAC模型,导致本身本应该只有几行代码且可以在项目级别复用的RBAC模型不仅不能复用,还要在每个项目无论是否需要都要有User、Role、Permission等实体类,更有甚者把实体类对应的数据表的结构和关联当作权限系统的核心。
(3)权限的抽象错误
我们通常既不实现操作系统也不实现数据库,虽然操作系统的权限和数据库的权限可以借鉴,但一般的业务系统上来就弄出一堆增删该查、访问和执行这样的权限,真是跑偏的太远了。首先业务层次的操作至少要从业务的含义出发,叫浏览、编辑、审核等这些客户容易理解或就是客户使用的词汇更有意义,更重要的是我们是从角色中按照最小授权需求抽象出来的权限,怎么什么都没做就有了一堆权限呢。
(4)将界面控制和权限耦合到一起
开始的时候我们只有实体类Entities、应用服务Service以及对一些采用接口隔离原则定义的接口Interfaces,通常这个时候我们在Service的一个或多个方法会对应1个权限,这个时候根本界面还没有,就算有界面,也是界面对权限的单向依赖,对于一个系统,可能不止有1个以上类型的客户端,每个客户端的界面访问控制对权限的依赖都应该存储到客户端,况且不同的客户端对这些数据各奔没有办法复用。
下面我们使用尽可能少的代码来构建一个可复用的既不依赖数据访问层也不依赖界面的RBAC模型,在此基础上对角色的稳定性和权限的抽象做一个总结。
1.创建RBAC模型
使用POCO创建基于RBAC0级别的可复用的User、Role和Permissin模型。
using System.Collections.Generic; namespace RBACExample.RBAC { public class RBACUser { public string UserName { get; set; } public ICollection<RBACRole> Roles { get; set; } = new List<RBACRole>(); } public class RBACRole { public string RoleName { get; set; } public ICollection<RBACPermission> Permissions { get; set; } = new List<RBACPermission>(); } public class RBACPermission { public string PermissionName { get; set; } } }
2.创建安全上下文
创建安全上下文RBACContext用于设置和获取RBACUser对象。RBACContext使用线程级别的静态变量保存RBACUser对象,不负责实体类到RBAC对象的转换,保证复用性。
using System; namespace RBACExample.RBAC { public static class RBACContext { [ThreadStatic] private static RBACUser _User; private static Func<string, RBACUser> _SetRBACUser; public static void SetRBACUser(Func<string, RBACUser> setRBACUser) { _SetRBACUser = setRBACUser; } public static RBACUser GetRBACUser(string username) { return _User == null ? (_User = _SetRBACUser(username)) : _User; } public static void Clear() { _SetRBACUser = null; } } }
3.自定义RoleProvider
自定义DelegeteRoleProvider,将权限相关的GetRolesForUser和IsUserInRole的具体实现委托给静态代理,保证复用性。
using System; using System.Web.Security; namespace RBACExample.RBAC { public class DelegeteRoleProvider : RoleProvider { private static Func<string, string[]> _GetRolesForUser; private static Func<string, string, bool> _IsUserInRole; public static void SetGetRolesForUser(Func<string, string[]> getRolesForUser) { _GetRolesForUser = getRolesForUser; } public static void SetIsUserInRole(Func<string, string, bool> isUserInRole) { _IsUserInRole = isUserInRole; } public override string[] GetRolesForUser(string username) { return _GetRolesForUser(username); } public override bool IsUserInRole(string username, string roleName) { return _IsUserInRole(username, roleName); } #region NotImplemented #endregion NotImplemented } }
在Web.config中配置DelegeteRoleProvider
<system.web> <compilation debug="true" targetFramework="4.5.2"/> <httpRuntime targetFramework="4.5.2"/> <authentication mode="Forms"> <forms loginUrl="~/Home/Login" cookieless="UseCookies" slidingExpiration="true" /> </authentication> <roleManager defaultProvider="DelegeteRoleProvider" enabled="true"> <providers> <clear /> <add name="DelegeteRoleProvider" type="RBACExample.RBAC.DelegeteRoleProvider" /> </providers> </roleManager> </system.web>
4.配置RBACContext和DelegeteRoleProvider
在Application_Start中配置RBACContext和DelegeteRoleProvider依赖的代理。为了便于演示我们直接创建RBACUser对象,在后文中我们再针对不同系统演示实体类到RBAC模型的映射。
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { RBACContext.SetRBACUser(u => { return new RBACUser { UserName = u, Roles = new List<RBACRole> { new RBACRole { RoleName="admin", Permissions = new List<RBACPermission> { new RBACPermission { PermissionName="admin" } } } } }; }); DelegeteRoleProvider.SetGetRolesForUser(userName => RBACContext.GetRBACUser(userName).Roles.SelectMany(o => o.Permissions).Select(p => p.PermissionName).ToArray()); DelegeteRoleProvider.SetIsUserInRole((userName, roleName) => RBACContext.GetRBACUser(userName).Roles.SelectMany(o => o.Permissions).Any(p => p.PermissionName == roleName)); AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); } }
5.在ASP.NET MVC中通过.NET API使用
User.IsInRole和AuthorizeAttribute此时都可以使用,我们已经完成了一个RBAC权限中间层,即隔离了不同系统的具体实现,也不用使用新的API调用。如果是服务层,使用Thread.CurrentPrincipal.IsInRole和PrincipalPermissionAttribute。
namespace RBACExample.Controllers { public class HomeController : Controller { public ActionResult Login(string returnUrl) { FormsAuthentication.SetAuthCookie("admin", false); return Redirect(returnUrl); } public ActionResult Logoff() { FormsAuthentication.SignOut(); return Redirect("/"); } public ActionResult Index() { return Content("home"); } [Authorize] public ActionResult Account() { return Content(string.Format("user is IsAuthenticated:{0}", User.Identity.IsAuthenticated)); } [Authorize(Roles = "admin")] public ActionResult Admin() { return Content(string.Format("user is in role admin:{0}", User.IsInRole("admin"))); } } }
6.扩展AuthorizeAttribute,统一配置授权
AuthorizeAttribute的使用将授权分散在多个Controller中,我们可以扩展AuthorizeAttribute,自定义一个MvcAuthorizeAttribute,以静态字典保存配置,这样就可以通过代码、配置文件或数据库等方式读取配置再存放到字典中,实现动态配置。此时可以从Controller中移除AuthorizeAttribute。如前文所述,客户端的访问控制与权限的匹配应该存储到客户端为最佳,即使存放到数据库也不要关联权限相关的表。
namespace RBACExample.RBAC { public class MvcAuthorizeAttribute : AuthorizeAttribute { private static Dictionary<string, string> _ActionRoleMapping = new Dictionary<string, string>(); public static void AddConfig(string controllerAction, params string[] roles) { var rolesString = string.Empty; roles.ToList().ForEach(r => rolesString += "," + r); rolesString = rolesString.TrimStart(','); _ActionRoleMapping.Add(controllerAction, rolesString); } public override void OnAuthorization(AuthorizationContext filterContext) { var key = string.Format("{0}{1}", filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName); if (_ActionRoleMapping.ContainsKey(key)) { this.Roles = _ActionRoleMapping[key]; base.OnAuthorization(filterContext); } } } }
通过GlobalFilterCollection配置将MvcAuthorizeAttribute配置为全局Filter。
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); MvcAuthorizeAttribute.AddConfig("AccountIndex"); MvcAuthorizeAttribute.AddConfig("AdminIndex", Permission.AdminPermission); filters.Add(new MvcAuthorizeAttribute()); }
7.按需设计实体类
当RBAC模型不直接依赖实体类时,实体类可以按需设计,不再需要为了迁就RBAC的关联引入过多的实体,可以真正做到具体问题具体分析,不需要什么系统都上Role、Permission等实体类,对于角色稳定的系统,既减少了系统的复杂度,也减少了大量后台的功能实现,也简化了后台的操作,不用什么系统都上一套用户头疼培训人员也头疼的权限中心。
(1)使用属性判断权限的系统
有些系统,比如个人博客,只有一个管理员角色admin,admin角色是稳定的权限不变的,所以既不需要考虑使用多个角色也不需要再进行权限抽象,因此使用User.IsAdmin属性代替Role和Permission就可以,没必要再使用Role和Permission实体类,增大代码量。后台进行权限管理只需要实现属性的编辑。
RBACContext.SetRBACUser(u => { var user = new UserEntity { UserName = "admin", IsAdmin = true }; var rbacUser = new RBACUser { UserName = user.UserName }; if (user.IsAdmin) { rbacUser.Roles.Add(new RBACRole { RoleName = "admin", Permissions = new List<RBACPermission> {new RBACPermission { PermissionName="admin" } } }); } return rbacUser; });
(2)使用角色判断权限的系统
有些系统,比如B2C的商城,虽然有多个角色,但角色都是稳定的权限不变的,使用User和Role就可以,没有必要为了应用RBAC而引入Permission类,强行引入虽然实现了Role和Permission的分配回收功能,但实际上不会使用,只会使用User的Role授权功能。权限的抽象要做到满足授权需求即可,在角色就能满足授权需求的情况下,角色和权限的概念是一体的。后台实现权限管理只需要实现对用户角色的管理。
(3)需要对角色进行动态授权的系统
有些系统,比如ERP,有多个不稳定的角色,每个角色通常对应多项权限,由于组织机构和人员职责的变化,必须对角色的权限进行动态分配,需要使用User、Role和Permission的组合。User由于权限范围的不同,通常具有一个或多个权限,不同的User具有的角色通常不再是平行关系而是层级关系,如果不从Role中抽象Permission,需要定义大量的Role对应不同权限的组合,遇到这种情况时,分离权限,对角色进行权限管理就成了必然。后台实现权限管理即需要实现对用户角色的管理也需要实现对角色权限的管理。
RBACContext.SetRBACUser(u => { var user = ObjectFactory.GetInstance<IUserService>().GetUserByName(u); return new RBACUser { UserName = user.UserName, Roles = user.Roles.Select(r => new RBACRole { RoleName = r.RoleName, Permissions = r.Permissions.Select(p => new RBACPermission { PermissionName = p.Name }).ToList() }).ToList() }; });
8.总结
使用RBAC模型和.NET的权限验证API解决了权限系统的复用问题,从角色的稳定性出发防止实体类规模膨胀,通过最小授权需求的抽象可以防止权限的滥用。
参考:
(1)https://en.wikipedia.org/wiki/Role-based_access_control
(2)http://csrc.nist.gov/groups/SNS/rbac/faq.html
(3)http://www.codeproject.com/Articles/875547/Custom-Roles-Based-Access-Control-RBAC-in-ASP-NET
(4)http://www.ibm.com/developerworks/cn/java/j-lo-rbacwebsecurity/