Entity Framework技术系列之8:使用Entity Framework技术实现RBAC模型
前言
RBAC(Role-Based Access Control,基于角色的访问控制),是继DAC(Discretionary Access Control,自主访问控制)和MAC(Mandatory Access Control,强制访问控制)后,于上世纪90年代提出的一种访问控制模型。RBAC模型能有效地解决DAC模型因客体所有者最终不能控制对该客体的所有访问许可而产生的安全漏洞问题;以及MAC模型控制过于严格,实现工作量太大,管理不便,不适用于主体或客体经常更新的应用环境等问题。
Ravi Sandhu等人于1996年提出了RBAC96模型,这也是目前应用最广泛的RBAC模型。该模型分为RBAC0、RBAC1、RBAC2和RBAC3四个层次。其中,RBAC0是最基本的模型,包含RBAC模型的核心部分(用户、角色、权限和会话);RBAC1在RBAC0的基础上定义了角色继承关系;RBAC2在RBAC0的基础上定义了限制,包括静态限制(如角色互斥、角色指派次数限制等)和动态限制(如某角色用户同时处于激活会话数量限制);RBAC3包含RBAC1和RBAC2,自然也包含RBAC0,是一个完整的RBAC模型。
设计模型
一、设计目标
本文将实现的RBAC模型(后文简称为RBAC模型),包括对RBAC0定义的用户、角色、权限的实现,但不实现会话;同时实现RBAC1定义的角色继承关系。
RBAC模型应提供接口,供上层应用实现用户身份认证、授权以及鉴权等安全控制。权限的控制粒度仅限于页面级,但需考虑对更细粒度权限控制的扩展需求。
二、模型架构
RBAC模型将提供包括实体数据模型和RBAC成员资格的实现。另外,还将提供一个演示程序,以展示该模型的最佳使用实践。RBAC模型架构如下图所示:
图1 RBAC模型架构图
三、E-R模型设计
要实现RBAC模型需求,需设计角色、用户、资源,以及用户指派(UA)和权限指派(PA)。角色支持继承关系,资源体现上下级关系。RBAC模型的E-R模型如下图所示:
图2 RBAC模型E-R模型图
实现模型
RBAC模型的实现主要包括实体数据模型和自定义成员资格提供程序两方面。
一、实体数据模型
实体数据模型的实现包括映射文件(SSDL、MSL和CSDL)、实体类和实体上下文环境的实现,具体实现细节请参见《Entity Framework技术系列之4:灵活应用实体数据模型》。实体关系图如下图所示:
图3 RBAC模型实体关系图
二、自定义成员资格提供程序
ASP.NET成员资格体系通过MembershipProvider和RoleProvider抽象成员资格提供程序,定义了成员资格的相关接口,并对外暴露Membership静态类、IPrincipal和IIdentity接口供ASP.NET应用调用。配合相关的自定义控件(如Login控件、LoginStatus控件等),可以快速实现用户身份认证功能。另外,.NET还提供了IPermission接口以及该接口的若干实现,支持灵活的权限控制。
但是,ASP.NET成员资格存在一些不足,以致其适用环境其实并不广泛。首先,虽然它提供了默认成员资格实现SqlMembershipProvider和SqlRoleProvide,但是该实现仅能使用SQL Server数据库进行数据持久化;其次,成员资格只包含了用户和角色信息,对资源丝毫没有涉及,这使得它仅能提供身份认证功能,授权和鉴权基本上可以理解为没有;如果硬要说成员资格也支持权限控制,那么该支持也是比较蹩脚的,它需要在开发时就确定权限,并进行硬编码,这感觉更像是给开发人员使用的权限控制,而不是面向用户的。而现实项目中更多的情况是需要在运行时动态管理资源和权限信息。
RBAC模型通过实现自定义RbacMembershipProvider和RbacRoleProvider成员资格提供程序来解决上述关于数据库局限问题。应用程序可以像使用SqlMembershipProvider和SqlRoleProvider成员资格提供程序一样使用RBAC模型自定义成员资格提供程序;同时,自定义成员资格提供程序也是很好的成员资格提供程序扩展示例,即使你不使用它,也可以参考它关于自定义成员资格提供程序的实现思路及细节。鉴于RbacMembershipProvider和RbacRoleProvider的内容都是使用实体数据模型实现各自基类的抽象方法,均容易理解,这里不再一一罗列代码。
但仍有两个地方需要罗嗦两句。首先是关于密码的安全问题,为了避免重蹈CSDN密码泄露事件的覆辙,RBAC模型提供了扩展方法供上层应用对密码做单向的SHA1摘要运算处理后存储(后续将专门开一个信息安全系列,会对对称加密、非对称加密、摘要运算、数字签名、数字信封、PKI、PMI等信息安全技术进行一一梳理,欢迎到时收看)。代码如下:
1 public static class Extensions 2 { 3 /// <summary> 4 /// 使用 SHA1 哈希算法对字符串进行单向加密 5 /// </summary> 6 /// <param name="plainText"></param> 7 /// <returns></returns> 8 public static string ToPassword(this string plainText) 9 { 10 var source = Encoding.Default.GetBytes(plainText); 11 12 // 对密码做SHA1摘要运算 13 var hash = SHA1.Create().ComputeHash(source); 14 15 // 将摘要值作Base64编码为可见字符 16 return Convert.ToBase64String(hash); 17 } 18 19 … 20 }
RbacMembershipProvider的ValidateUser方法使用了同样的摘要运算后进行比对:
1 if (user.Password != password.ToPassword()) 2 throw new MembershipException("密码错误");
RbacRoleProvider的GetRolesForUser方法体现了角色的继承思想,即一个用户拥有某角色,则他也同时拥有了该角色的父角色。代码如下:
1 public override string[] GetRolesForUser(string userName) 2 { 3 var roleNames = new List<string>(); 4 5 using (var db = new EntityContext()) 6 { 7 var user = db.Users.FirstOrDefault(o => o.Name == userName); 8 9 if (user == null) 10 throw new EntityNotFoundException(string.Format("用户 {0} 不存在", userName)); 11 12 foreach (var role in user.Roles) 13 { 14 var item = role; 15 16 // 继承父级角色 17 do 18 { 19 roleNames.Add(item.Name); 20 item = role.Parent; 21 } while (item != null); 22 } 23 } 24 25 return roleNames.Distinct().ToArray(); 26 }
RBAC模型通过扩展定义抽象资源提供程序ResourceProvider,以解决上述授权和鉴权问题。ResourceProvider定义了资源的授权、鉴权以及权限信息查询等接口,如下所示:
1 public abstract class ResourceProvider : ProviderBase 2 { 3 public abstract void Authorize(string resourceUri, string[] roleNames); 4 5 public abstract void AddRolesToResources(string[] resourceUris, string[] roleNames); 6 7 public abstract string[] FindRolesHasResource(string resourceUri); 8 9 public abstract string[] GetResourcesForRole(string roleName); 10 11 public abstract bool IsRoleHasResource(string roleName, string resourceUri); 12 13 public abstract void RemoveRolesFromResource(string[] roleNames, string[] resourceUris); 14 15 public abstract string[] GetAllResources(); 16 17 public abstract IPermission GetPermissionForResource(string resourceUri); 18 19 public abstract void Authenticate(); 20 }
由上面代码可见,ResourceProvider继承自ProviderBase,结合对提供程序相关配置类的实现,可以支持通过配置Web.config接入资源提供程序。相关的配置类包括ResourceManagerSection、ResourceProviderCollection和RbacMembership静态类。
ResourceManagerSection类:
1 public class ResourceManagerSection : ConfigurationSection 2 { 3 [ConfigurationProperty("providers")] 4 public ProviderSettingsCollection Providers 5 { 6 get { return (ProviderSettingsCollection)base["providers"]; } 7 } 8 9 [StringValidator(MinLength = 1)] 10 [ConfigurationProperty("defaultProvider", DefaultValue = "RbacResourceProvider")] 11 public string DefaultProvider 12 { 13 get { return (string)base["defaultProvider"]; } 14 set { base["defaultProvider"] = value; } 15 } 16 }
ResourceProviderCollection类:
1 public class ResourceProviderCollection : ProviderCollection 2 { 3 public ResourceProvider this[string name] 4 { 5 get { return (ResourceProvider)base[name]; } 6 } 7 8 public override void Add(ProviderBase provider) 9 { 10 if (provider == null) 11 throw new ArgumentNullException("Provider is null"); 12 13 if (!(provider is ResourceProvider)) 14 throw new ArgumentException("Provider is not ResourceProvider"); 15 16 base.Add(provider); 17 } 18 }
RbacMembership静态类
1 public static class RbacMembership 2 { 3 public static ResourceProvider ResourceProvider 4 { 5 get 6 { 7 var section = (ResourceManagerSection)ConfigurationManager.GetSection("resourceManager"); 8 return ResourceProviders[section.DefaultProvider]; 9 } 10 } 11 12 private static ResourceProviderCollection resourceProviders; 13 public static ResourceProviderCollection ResourceProviders 14 { 15 get 16 { 17 if (resourceProviders == null) 18 { 19 resourceProviders = new ResourceProviderCollection(); 20 var section = (ResourceManagerSection)ConfigurationManager.GetSection("resourceManager"); 21 22 ProvidersHelper.InstantiateProviders(section.Providers, resourceProviders, typeof(ResourceProvider)); 23 } 24 25 return resourceProviders; 26 } 27 } 28 }
RBAC模型成员资格模型提供了RbacResourceProvider作为ResourceProvider的默认实现。RbacResourceProvider的绝大部分内容很容易理解,都是使用实体数据模型实现各抽象方法,这里不再赘述。但鉴权方法的实现需要特别说明,RbacResourceProvider结合使用.NET的权限控制思想和RBAC模型的权限管理机制,实现了页面级的权限控制,代码如下:
1 public override void Authenticate() 2 { 3 IPermission permission = new PrincipalPermission(null, "初始化人员"); 4 var roles = this.FindRolesHasResource(HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath); 5 6 foreach (var role in roles) 7 { 8 permission = permission.Union(new PrincipalPermission(HttpContext.Current.User.Identity.Name, role)); 9 } 10 11 permission.Demand(); 12 }
最后,来看看RBAC模型成员资格模型架构,如下图所示:
图4 RBAC成员资格模型架构图
应用模型
本文提供了一个演示程序,演示如何使用RBAC模型实现用户、角色和资源信息的持久化,以及身份认证、授权和鉴权等安全控制功能。
一、数据持久化
使用RBAC模型的实体数据模型,可以对用户、角色和资源信息进行快速持久化操作。关于实体数据模型数据绑定,可参见《Entity Framework技术系列之6:数据绑定》,这里不再赘述,直接上效果图:
图5 用户管理效果图
图6 角色管理效果图
图7 资源管理效果图
二、身份认证
ASP.NET提供了Login自定义控件,与成员资格协作,使身份认证变得很简单。首先,需要在Web.config中配置成员资格提供程序信息:
1 <?xml version="1.0" encoding="utf-8"?> 2 <configuration> 3 … 4 <system.web> 5 … 6 <authentication mode="Forms"> 7 <forms loginUrl="~/Login.aspx" defaultUrl="~/Main/Conference/Default.aspx"/> 8 </authentication> 9 <membership defaultProvider="RbacMembershipProvider" userIsOnlineTimeWindow="15"> 10 <providers> 11 <clear/> 12 <add name="RbacMembershipProvider" type="Apollo.Blog.EF.Chapter8.Membership.RbacMembershipProvider"/> 13 </providers> 14 </membership> 15 <roleManager defaultProvider="RbacRoleProvider" enabled="true" cookieTimeout="15" cacheRolesInCookie="true" cookieName=".RBACROLES"> 16 <providers> 17 <clear/> 18 <add name="RbacRoleProvider" type="Apollo.Blog.EF.Chapter8.Membership.RbacRoleProvider"/> 19 </providers> 20 </roleManager> 21 … 22 </system.web> 23 … 24 </configuration>
然后,在登录页面加入Login控件:
1 <asp:Login ID="Login1" runat="server" DestinationPageUrl="~/Desktop.aspx" MembershipProvider="RbacMembershipProvider"> 2 <LayoutTemplate> 3 <table> 4 <tbody> 5 <tr> 6 <th>Account:</th> 7 <td><asp:TextBox ID="UserName" runat="server" Text="Apollo" /></td> 8 </tr> 9 <tr> 10 <th>Password:</th> 11 <td><asp:TextBox ID="Password" runat="server" TextMode="Password" /></td> 12 </tr> 13 </tbody> 14 <tfoot> 15 <tr> 16 <td colspan="2"> 17 <asp:Button ID="btnLogin" runat="server" Text="Login" CommandName="Login" SkinID="Large" ValidationGroup="Login" /> 18 </td> 19 </tr> 20 </tfoot> 21 </table> 22 <div class="tip" style="width:340px; height:20px;"> 23 <asp:Label ID="FailureText" runat="server" SkinID="FailureText" Text="<br>" /> 24 </div> 25 </LayoutTemplate> 26 </asp:Login>
然后,就不需要然后了。ASP.NET会在用户登录时,调用Login控件设置的MembershipProvider(如果不显式设置,则调用Web.config中配置的默认提供程序)的ValidateUser(…)方法进行身份认证。
三、授权
演示程序使用ResourceProvider提供程序实现授权功能。授权功能包括用户授权(将用户指派给角色,即UA。注意,通常我们都说把角色指派给用户,但鉴于RBAC模型是以角色为中心的,所以这样说其实更严谨)和资源授权(将权限指派给角色,即PA)。
要使用ResourceProvider提供程序,需要在Web.config中加入相应的配置:
1 <?xml version="1.0" encoding="utf-8"?> 2 <configuration> 3 <configSections> 4 <section name="resourceManager" type="Apollo.Blog.EF.Chapter8.Membership.ResourceManagerSection, Apollo.Blog.EF.Chapter8.Membership" /> 5 </configSections> 6 <resourceManager defaultProvider="RbacResourceProvider"> 7 <providers> 8 <clear/> 9 <add name="RbacResourceProvider" type="Apollo.Blog.EF.Chapter8.Membership.RbacResourceProvider"/> 10 </providers> 11 </resourceManager> 12 … 13 </configuration>
然后,就可以使用RbacMembership静态类的ResourceProvider属性获取默认资源提供程序,以及通过ResourceProviders获取已配置的所有资源提供程序集合。
这里就不对用户授权和资源授权的页面前端代码进行分析了,都是直接调用ResourceProvider的相关方法,很好理解,直接上效果图:
图8 用户授权效果图
图9 资源授权效果图
四、鉴权
由于RbacResourceProvider提供程序实现了ResourceProvider定义的Authenticate抽象方法,页面只需要简单的调用就可以了。本程序选择了在相应的母板页中进行鉴权操作:
1 public partial class Workplace : System.Web.UI.MasterPage 2 { 3 protected void Page_Load(object sender, EventArgs e) 4 { 5 if (!Page.IsPostBack) 6 { 7 RbacMembership.ResourceProvider.Authenticate(); 8 } 9 } 10 }
遗留问题
RBAC模型存在一些问题或值得改进的地方,这里罗列出来,欢迎大家讨论。
一、权限粒度问题
RBAC模型可以将权限控制到页面级,但这在项目中往往是不够的。在开发时,可能需要控制到方法,甚至有可能在任意地方加入控制;在运行时,可能需要将粒度控制在控件级。针对前者,可以借鉴.NET的PrincipalPermissionAttribute,但是如何实现动态授权及鉴权是个问题;针对后者,RBAC模型的资源及权限管理本身是支持扩展至任何粒度的,只是鉴权操作如何切入也是个问题。
二、菜单显示问题
菜单的显示应做到针对不同的用户,仅显示其有权访问的链接。演示程序的AccordionMenu自定义控件其实已支持该功能,只是该控件需要在sitemap文件中进行权限配置。演示程序提供了一个静态的sitemap配置,内容如下:
1 <?xml version="1.0" encoding="utf-8" ?> 2 <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0"> 3 <siteMapNode url="~/Desktop.aspx" title="Apollo RBAC Model"> 4 <siteMapNode title="Console" url="~/Main/Console" roles="Employee"> 5 <siteMapNode url="~/Main/Console/User/Default.aspx" title="User Management" /> 6 <siteMapNode url="~/Main/Console/Role/Default.aspx" title="Role Management" /> 7 <siteMapNode url="~/Main/Console/Resource/Default.aspx" title="Resource & Permission" /> 8 </siteMapNode> 9 </siteMapNode> 10 </siteMap>
但这其实是没有多大意义的,因为资源及权限是在运行时动态管理的,一旦变更,就会与sitemap中的配置不符。可以通过在资源管理和权限管理的同时,动态生成sitemap文件来解决该问题。
注意,这里一直说的是“sitemap文件”,而没有直接说“Web.sitemap文件”,是因为站点地图文件不一定非要叫“Web.sitemap”。如果使用其他名称,请在Web.config文件中显式指定:
1 <?xml version="1.0" encoding="utf-8"?> 2 <configuration> 3 … 4 <system.web> 5 … 6 <siteMap defaultProvider="Apollo" enabled="true"> 7 <providers> 8 <clear/> 9 <add name="Apollo" siteMapFile="Apollo.sitemap" securityTrimmingEnabled="true" type="System.Web.XmlSiteMapProvider, System.Web, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/> 10 </providers> 11 </siteMap> 12 … 13 </system.web> 14 </configuration>
三、分布式应用
RBAC模型只提供了实体数据模型和自定义成员资格提供程序,应用程序只能将其集成到本地。其实我们还可以做得更多,分布式应用可能会让你的思路开阔不少。通过扩展,使RBAC模型提供分布式服务,以SaaS的方式,托管应用系统的用户、角色、资源及权限信息,此时,你会发现其实是将RBAC模型由一个中间件扩展成了一个PaaS,这已经是一个可以单独销售的产品了,甚至你可以自己运营它。
更多的改进建议,欢迎各位讨论。
关于源码
本文附件提供了完整的RBAC模型源码解决方案,包括RBAC模型类库、RBAC模型测试项目和演示程序。
由于时间原因,未对自定义成员资格提供程序做单元测试,若发现BUG,会即时更新。也欢迎读者指出BUG和改进建议,以使RBAC模型不断完善。
如你所见,演示程序模仿了Sharepoint2007的UI风格,这仅仅是出于我对该风格的偏好,你可以通过实现其他主题,以提供不一样的UI风格及用户体验。
运行演示程序需要顺序做以下几件事:
一、创建数据库
你可以在“\Apollo.Blog.EF.Chapter8.Membership\Edm\Mapping\”目录下找到数据库初始化脚本。由于精力有限的原因,仅提供了SQL Server版本的初始化脚本Membership.SQLServer.sql及映射文件Membership.SQLServer.ssdl,若需要扩展支持其他数据库,请参见《Entity Framework技术系列之4:灵活应用实体数据模型》一文中,关于多数据库支持的内容,实现Membership.OtherDatabase.sql和Membership. OtherDatabase.ssdl。
二、初始化数据
测试项目提供了一个名为Initial.orderedtest顺序测试文件,运行该顺序测试,就可以建立运行演示程序所需的用户、角色、资源和权限数据了。
三、Show Time
浏览Apollo.Blog.EF.Chapter8.Membership.Demo\Login.aspx页面,可以进入如下的登录页面:
图10 登录页面效果图
初始数据提供了Apollo\11111111作为默认的帐号和口令,登录后将看到如下的桌面页面:
图11 桌面页面效果图
接下来,就请尽情体验Entity Framework技术实现RBAC模型及其演示程序的方方面面吧,相信会有一些东西会是你感兴趣的。
你可以将本文源码用于任何用途,唯一不被允许的是将本源码直接作为商品进行销售。
总结
本文首先简单介绍了RBAC0-RBAC3模型;然后提出RBAC模型的设计目标,并进行了概要设计;然后使用Entity Framework作为持久层技术,实现了实体数据模型和自定义成员资格提供程序;最后,通过演示程序,讲解了如何将RBAC模型应用到项目中。作为本系列的完结篇,本文基本覆盖了前文的所有知识点,包括DIY实体数据模型、各种对象-关系映射、延迟加载、数据绑定、LINQ to Entities以及多数据库支持等。