前言
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以及多數據庫支持等。