[一步一步MVC]第二回:還是ActionFilter,實現對業務邏輯的統一Authorize處理
系列文章導航:
[一步一步MVC]第一回:使用ActionSelector控制Action的選擇
[一步一步MVC]第二回:還是ActionFilter,實現對業務邏輯的統一Authorize處理
[一步一步MVC]第四回:漫談ActionLink,有時“胡攪蠻纏”
[一步一步MVC]第五回:讓TagBuilder豐富你的HtmlHelper
由問題引出
在ASP .NET MVC中,以友好的URL訪問資源是MVC吸引眼球的特色之一,但是隨之而來對于Authorize問題的處理變得令人令人頭痛。例如假設我們有一個獲取Book信息的Action,定義在BookController中:
{
// Release : code01, 2009/04/22
// Author : Anytao, http://www.anytao.com
public ActionResult Index(int id)
{
Book model = (new IBookService()).GetBook(id);
return View(model);
}
}
那么,我們可以通過http://anytao.net/Book/index/1,來訪問id為1的Book(例如該書是《你必須知道的.NET》,哈哈,廣告嫌疑)。在沒有任何特別處理的情況下,對于該書的訪問是“不設防”的。任何用戶可以通過http://anytao.net/Book/index/1實現對《你必須知道的.NET》信息的訪問。那么訪問的資源如果是http://anytao.net/Secret/index/1,顯然我的秘密無一例外的對外公開了。
言之此處,我們的問題已經明白無疑,那么應該如何處理呢?我們可以很容易的想到通過以下的方式進行處理:
// Release : code02, 2009/04/22 // Author : Anytao, http://www.anytao.com public ActionResult Index(int id) { if (new IAuthorizeService().IsBookAuthorized(id, User.Identity.Name)) { Book model = (new IBookService()).GetBook(id); return View(model); } else { return View("NotValid"); } }
顯然,我通過IsBookAuthorized對GetBook服務的訪問有效性進行控制,通過User的Name在數據庫或者其他資源存儲進行查找, 然后根據IsBookAuthorized結果進行是否訪問的控制,顯然不合法的用戶將被導航到NotValid頁,提示你是非法用戶。
這種方式顯然是最容易想到的辦法,而且也廣泛存在于我們實際的應用中,例如NerdDinner范例中也是通過這種方式進行Authorize控制處理的,例如:
[Authorize] public ActionResult Edit(int id) { Dinner dinner = dinnerRepository.GetDinner(id); if (!dinner.IsHostedBy(User.Identity.Name)) return View("InvalidOwner"); return View(new DinnerFormViewModel(dinner)); }
然而這種方式存在或多或少的問題,例如:
- IsBookAuthorized將分散于不同的Action或者BLL層中,對于統一的Authorized管理帶來問題。
- 實際的Authorized執行已經滲透到Action或者Serivce內部,我們更期待在Action調用之前對此已經進行了處理。
思考的瞬間
那么,統一的處理該如何著手實現更優雅的、更統一的Authorize處理呢?顯然MVC自帶的Authorize特性,為我們提供了可選擇的思路:
[Authorize(Users = "Anytao")] public ActionResult Edit(int id) { return View(); }
Authorize標記通過對于Users或者Roles的定義,來對Edit Action的執行進行“預”Authorize授權,那么登陸用戶為Anytao的用戶才有權對BookController Edit進行訪問,否則將無權訪問。顯然,這種方式對于滿足我們
- 統一Authorize處理
- 在Action調用之前進行授權驗證
的目標是統一的。所以,我們可以借助這種方式實現自定義的統一Authorize處理方案。
統一Authorize解決方案
有了指導方針,我們就可以有的放肆了,我們的方案同樣是應用ActionFilter實現對Authorize處理,上次的范例是{[一步一步MVC]第四回:使用ActionSelector控制Action的選擇}。顯然我們可以在OnActionExecuting事件中對Action進行“預”處理,將關于Authorize的驗證過程統一在OnActionExecuting中進行,就可以對標記的Action實現調用之前的過濾了,所以我們首先實現一個AuthorizeAttributeBase,例如:
// Release : code03, 2009/04/22 // Author : Anytao, http://www.anytao.com public abstract class AuthorizeAttributeBase : ActionFilterAttribute { public AuthorizeAttributeBase() { } public AuthorizeAttributeBase(string key) { Key = key; } public override void OnActionExecuting(ActionExecutingContext filterContext) {
// Authorize handler } public string Key { get; set; } protected abstract bool IsAuthorized(int id); }
而具體的驗證則在具體實現類中,例如我們對Book的驗證:
// Release : code04, 2009/04/22 // Author : Anytao, http://www.anytao.com public class BookAuthorizeAttribute : AuthorizeAttributeBase { protected override bool IsAuthorized(int id) { return (new IAuthorizeService()).IsBookAuthorized(id); } }
對于驗證的處理必須解決兩方面的問題:
- 在AuthorizeAttributeBase中獲取待過濾Action中的參數(Index(int id)),一般而言我們需要對id進行驗證,那么傳入id的值該如何處理。
- 在AuthorizeAttributeBase對于非法用戶的處理,一般而言就是導航到NotValid頁面。
在OnActionExecuting中獲取Action參數
我們采用的方法是通過filterContext的ActionParameters來獲取參數值,通過參數的Key來獲取其值,例如:
if (filterContext.ActionParameters.ContainsKey(key)) { value = int.Parse(filterContext.ActionParameters[key].ToString()); }
在OnActionExecuting中導航到不同的View
這也是一個簡單的處理,我們只要指定好filterContext的Result為指定的ViewResult即可實現我們的目標:
filterContext.Result = new ViewResult{ ViewName = "NotValid" };
解決了上述問題,就基本實現了對Authorize進行統一處理的目標,至于具體的Authorize邏輯,不同的業務可以在不同的業務層進行封裝。例如對于Book資源的處理可以統一在IBookService中,對于User資源的處理可以統一在IUserService中(不過顯然我們已經有了MVC自帶的Authorize,不必重復),對于其他的資源也相應的處理在不同的業務層中。
下面是AuthorizeAttributeBase和BookAuthorizeAttribute的完整代碼:
// Release : code03, 2009/04/22 // Author : Anytao, http://www.anytao.com public abstract class AuthorizeAttributeBase : ActionFilterAttribute { public AuthorizeAttributeBase() { } public AuthorizeAttributeBase(string key) { Key = key; } public override void OnActionExecuting(ActionExecutingContext filterContext) { string key = string.IsNullOrEmpty(Key) ? "id" : Key; int id; if (filterContext.ActionParameters.ContainsKey(key)) { if (!int.TryParse(filterContext.ActionParameters[key].ToString(), out id)) { id = 0; } } else { id = 0; } if (id > 0) { if (IsAuthorized(id)) { base.OnActionExecuting(filterContext); } else { filterContext.Result = new ViewResult{ ViewName = "NotValid" }; } } else { filterContext.Result = new ViewResult{ ViewName = "NotValid" }; } } public string Key { get; set; } protected abstract bool IsAuthorized(int id); }
接下來就是如何應用了。
在Controller中應用統一Authorize處理
下面是我們的應用,還是對于http://anytao.net/Book/index/1的訪問,我們可以像下面這樣應用:
// Release : code05, 2009/04/22 // Author : Anytao, http://www.anytao.com [BookAuthorize(Key="id")] public ActionResult Index(int id) { Book model = (new IBookService()).GetBook(id); return View(model); }
對比前后的兩種方案,我想孰優孰劣顯而易見。BookAuthorize顯然以更優雅的方式實現了對于Authorize這回事兒的處理,也基本達到了原來的目標。我們的驗證邏輯沒有散落在系統四處,如何同時需要對Book的Index進行多個邏輯的驗證,我們的方式也變得很簡單,例如:
// Release : code05, 2009/04/22 // Author : Anytao, http://www.anytao.com [BookAuthorize(Key="id"), TaskAuthorize(Key="id")] public ActionResult Index(int id) { Book model = (new IBookService()).GetBook(id); return View(model); }
不過,我們需要對id的復用進行一點思考,不過那已經是另外一回兒事兒了。對本文而言,我已經達到了目標。當然,這也許不是最好的方案,所以我期待您的更好方案,因為技術需要切磋和共享。
又是一個小技巧,希望給你幫助。
代碼下載[anytao_mvc_actionauthorize],更多關注,盡在anytao.net/blog