在有些情況下,我們希望界面上的Action Link不僅僅是限制未授權用戶的進一步訪問,而是對於這些用戶直接隱藏。比如,以普通用戶登錄時,只能在頁面上看到一些常規的鏈接,而以管理員身份登錄時,除了能看到這些常規鏈接外,還能夠看到網站管理的鏈接。本文將介紹如何使用自定義的AuthorizeAttribute來實現這樣的功能。
為了方便介紹,在這里不打算使用那些復雜的權限管理子系統或者權限驗證機制,我們就做一個非常簡單的假設:如果輸入的用戶名是“daxnet”,則表示這個賬戶是一個管理員賬戶,否則,它就是一個普通用戶賬戶。在實際應用過程中,讀者朋友可以采用自己的一套權限驗證邏輯來判斷某個賬戶是否為管理員。
WCF Service
在《ASP.NET MVC實用技術:開篇》一文,我已經介紹過了三種不同的ASP.NET MVC應用模式,其中第二種“Data Transfer Object as View Model”是在企業級應用中最為常見的一種方式,本文(及以后的系列文章)都會使用這種應用模式進行介紹。所以,在這里我們也還是需要建立一個WCF Service,用來向客戶端返回登錄用戶權限認證的結果。
新建一個WCF Service,在其中定義一個enum類型,用來表示登錄用戶的賬戶類型。在這里我們只討論兩種類型:RegularUser,表示普通用戶賬戶;SiteAdmin,表示網站管理員賬戶。這個enum類型定義如下:
/// <summary> /// Represents the type of the account. /// </summary> [Flags] public enum AccountType { /// <summary> /// Indicates that the account is a regular user. /// </summary> [EnumMember] RegularUser = 1, /// <summary> /// Indicates that the account is the site administrator. /// </summary> [EnumMember] SiteAdmin = 2 }
使用FlagsAttribute來標記這個AccountType枚舉,是為了今后能夠更方便地處理更多類型的賬戶,事實上在我們這個案例中,並沒有太大的實際意義。
然后,新建一個Service Contract,為了簡化案例,這個Service Contract只包含一個操作,就是根據傳入的用戶賬戶名稱,返回AccountType。
[ServiceContract(Namespace="http://aspnetmvcpractice.com")] public interface IAccountService { [OperationContract] AccountType GetAccountType(string userName); }
之后在WCF Service中實現這個接口,根據我們上面的約定,當用戶名為“daxnet”的時候,就返回SiteAdmin,否則就返回RegularUser,因此這個實現類還是非常簡單的:
public class AccountService : IAccountService { #region IAccountService Members public AccountType GetAccountType(string userName) { if (userName == "daxnet") return AccountType.SiteAdmin; else return AccountType.RegularUser; } #endregion }
至此,我們完成了WCF Service部分的開發,接下來,需要在ASP.NET MVC中使用這個WCF Service來完成用戶的驗證操作。在通常情況下,我們會在ASP.NET MVC的應用程序上直接添加WCF Service的引用,這樣做其實也沒有什么太大的問題,不過我還是比較習慣另外新建一個Class Library,然后將WCF Service Reference添加到這個Class Library上,這樣做的好處是,可以把所有與ASP.NET MVC擴展相關的內容都集中起來,而且這種擴展相關的類型和方法都有可能需要用到WCF Service提供的服務,這樣也不至於將ASP.NET MVC應用程序的結構弄得很亂。在這個案例中,我們新建一個名為WebExtensions的Class Library,在這個Library中使用剛剛創建好的WCF Service來實現我們的自定義授權特性。
Web Extensions
CustomAuthorizeAttribute
在新建的這個Class Library中直接添加WCF Service Reference,這將在這個Library中產生一系列的代理類型,以及一個app.config文件。不要去關注這個app.config文件,因為它在這個Class Library中並不起什么作用;但是也不要去刪除這個文件,因為后面我們還是需要用到它里面的內容的。
在Class Library中,新建一個CustomAuthorizeAttribute類,使這個類繼承於AuthorizeAttribute。我們會在后面將這個Attribute用在action上,以限制未授權用戶對頁面的訪問。在這個類中,重載AuthorizeCore方法,它的處理邏輯如下:首先判斷當前賬戶是否被認證,如果沒有,則返回false;然后調用WCF Service來獲取當前賬戶的類型,並跟給定的類型進行比較,如果類型相同,則返回true,否則返回false。假設這個給定的賬戶類型是通過CustomAuthorizeAttribute類的構造函數傳入的,那么,當我們在某個action上應用[CustomAuthorizeAttribute(AccountType.SiteAdmin)]這個特性的時候,只要訪問這個action的用戶賬戶不是SiteAdmin,程序就會自動跳轉到登錄頁面,請求用戶以網站管理員的身份登錄。CustomAuthorizeAttribute類的代碼如下:
public class CustomAuthorizeAttribute : AuthorizeAttribute { private readonly AccountType requiredType; public CustomAuthorizeAttribute(AccountType comparedWithType) { this.requiredType = comparedWithType; } internal bool PerformAuthorizeCore(System.Web.HttpContextBase httpContext) { return this.AuthorizeCore(httpContext); } protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext) { if (httpContext == null) throw new ArgumentNullException("httpContext"); if (!httpContext.User.Identity.IsAuthenticated) return false; if (this.requiredType == (AccountType.SiteAdmin | AccountType.RegularUser)) return true; using (AccountServiceClient client = new AccountServiceClient()) { var calculatedAccountType = client.GetAccountType(httpContext.User.Identity.Name); switch(this.requiredType) { case AccountType.RegularUser: if ((calculatedAccountType & AccountType.RegularUser) == AccountType.RegularUser) return true; else return false; case AccountType.SiteAdmin: if ((calculatedAccountType & AccountType.SiteAdmin) == AccountType.SiteAdmin) return true; else return false; default: return base.AuthorizeCore(httpContext); } } } }
在這個類中有一個internal的方法:PerformAuthorizeCore,它的作用就是向程序集的其它方法暴露AuthorizeCore的執行邏輯,以避免相同的邏輯需要在程序集內部的其它類型中重復實現。這個PerformAuthorizeCore的方法會在自定義的HtmlHelper擴展方法中使用,目的就是為了能夠對未授權的賬戶隱藏Action Link。
HtmlHelper Extension
現在我們來擴展HtmlHelper類,使得其中的ActionLink方法能夠支持對未授權賬戶的隱藏。同樣也是在當前這個Class Library中,新建一個靜態類,命名為MvcExtensions,然后使用下面的代碼實現這個類:
public static class MvcExtensions { private static bool Visible(HtmlHelper helper, AccountType accountType) { return new CustomAuthorizeAttribute(accountType).PerformAuthorizeCore(helper.ViewContext.HttpContext); } /// <summary> /// Returns an anchor element (a element) that contains the virtual path of the specified action. /// </summary> /// <param name="htmlHelper">The HTML helper instance that this method extends.</param> /// <param name="linkText">The inner text of the anchor element.</param> /// <param name="actionName">The name of the action.</param> /// <param name="controllerName">The name of the controller.</param> /// <param name="accountTypeRequired">The required account type.</param> /// <returns>The anchor element (a element) that contains the virtual path of the specified action.</returns> public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, AccountType accountTypeRequired) { MvcHtmlString link = MvcHtmlString.Empty; if (Visible(htmlHelper, accountTypeRequired)) link = htmlHelper.ActionLink(linkText, actionName, controllerName); return link; } }
這個ActionLink方法首先將link設置為MvcHtmlString.Empty,表示為一個空的string,然后調用私用靜態方法Visible,來判斷當前用戶是否應該看到這個ActionLink,如果Visible返回的是true,則直接調用HtmlHelper中已有的ActionLink重載方法,否則直接返回MvcHtmlString.Empty。在Visible方法中,我們可以看到,所執行的邏輯正是CustomAuthorizeAttribute中的AuthorizeCore方法。
接下來要做的,就是在ASP.NET MVC應用程序中使用這些擴展方法和自定義特性。
ASP.NET MVC應用程序
在ASP.NET MVC應用程序上添加對上述Class Library的引用,然后我們打開Views\Shared\_Layout.cshtml文件,在這個Razor View中添加對所需命名空間的引用:
然后,根據需要,我們向主菜單中添加兩個ActionLink:Regular Users Only和Site Admins Only,前者僅允許普通用戶訪問,后者僅允許站點管理員訪問。在此所使用的ActionLink就是在上文中我們自定義的那個重載:
接下來在HomeController中定義兩個action:RegularUserVisible和SiteAdminVisible,並將CustomAuthorizeAttribute應用在這兩個action上。事實上這個步驟與隱藏Action Link並沒有太大關系,只是確保用戶無法通過在瀏覽器中輸入URL而直接訪問到這兩個頁面。
最后別忘了把Class Library下app.config中有關system.serviceModel的配置復制到ASP.NET MVC應用程序的web.config中。
運行程序
現在讓我們來啟動程序,看看會產生什么效果。首先啟動WCF Service,然后直接運行ASP.NET MVC應用程序,得到如下界面:
現在點擊“Log On”鏈接,以daxnet賬戶登錄,我們得到了如下的效果,可以看到頁面上顯示了“Site Admins Only”的鏈接選項:
退出登錄,再以“acqy”賬戶登錄,我們又得到了如下效果,看到頁面上顯示了“Regular Users Only”的選項:
本文案例源代碼下載
下載鏈接
請單擊此處下載本文案例源代碼
有關數據庫配置
本文使用的是SQL Server Enterprise Edition作為ASP.NET MVC的后台數據庫,如果你打算選用SQL Server Express作為數據庫,請修改本文案例中web.config里的連接字符串,並使用《ASP.NET MVC實用技術:開篇》一文中所介紹的方法重建你的數據庫結構。根據本文案例需要,你需要在ASP.NET MVC應用程序啟動以后,新建兩個用戶賬戶:daxnet以及另一個任意名稱的賬戶。當你正確地配置好了ASP.NET MVC的數據庫以后,你可以在Solution Explorer中單擊ASP.NET Configuration按鈕來配置你的ASP.NET MVC站點,以添加所需的用戶賬戶: