在 MVC 應用中,URL 直接對應 Action 方法,授權檢查若分散寫在每個 Action 內,不僅違反 DRY 原則,更難維護。本文示範如何透過 ActionFilter 實作集中式授權攔截,將授權邏輯從 Action 移至 Filter,達成關注點分離。
假設 BookController 有 Index(int id) Action,需驗證用戶是否擁有該書籍存取權:
public ActionResult Index(int id)
{
if (new AuthorizeService().IsBookAuthorized(id, User.Identity.Name))
{
var book = new BookService().GetBook(id);
return View(book);
}
return View("NotAuthorized");
}
缺點:
繼承 FilterAttribute、IAuthorizationFilter,在 OnAuthorization 方法中實作邏輯:
using System;
using System.Web.Mvc;
namespace MyProject.Filters
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class BookAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null) throw new ArgumentNullException(nameof(filterContext));
// 1. 取得路由參數 bookId
var routeData = filterContext.RouteData;
var bookIdStr = routeData.Values["id"]?.ToString();
if (!int.TryParse(bookIdStr, out int bookId))
{
HandleUnauthorizedRequest(filterContext, "無效的書籍識別碼。");
return;
}
// 2. 取得當前用戶身分
var userName = filterContext.HttpContext.User.Identity.Name;
if (string.IsNullOrEmpty(userName))
{
HandleUnauthorizedRequest(filterContext, "請先登入。");
return;
}
// 3. 呼叫授權服務(建議透過 DI 注入)
var authService = DependencyResolver.Current.GetService<IAuthorizeService>();
if (authService == null || !authService.IsBookAuthorized(bookId, userName))
{
HandleUnauthorizedRequest(filterContext, "您無權存取此書籍。");
return;
}
// 授權通過,繼續執行 Action
}
private void HandleUnauthorizedRequest(AuthorizationContext filterContext, string message)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
Data = new { success = false, message },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else
{
filterContext.Result = new ViewResult
{
ViewName = "NotAuthorized",
ViewData = new ViewDataDictionary<string>(message)
};
}
}
}
}
[BookAuthorize]
public class BookController : Controller
{
public ActionResult Index(int id) { ... }
[BookAuthorize] // 可覆蓋或疊加
public ActionResult Edit(int id) { ... }
}
避免在 Filter 中直接 new AuthorizeService(),透過 IDependencyResolver 或 ASP.NET Core 內建 DI 取得服務實例:
// ASP.NET MVC 5 使用 Unity/Autofac 等容器註冊
container.RegisterType<IAuthorizeService, AuthorizeService>();
// Filter 中解析
var authService = filterContext.HttpContext.Request.RequestContext
.HttpContext.ApplicationInstance
.Container.Resolve<IAuthorizeService>();
| 特性 | [Authorize] | 自訂 BookAuthorizeAttribute |
|---|---|---|
| 角色/用戶名稱驗證 | 支援 | 需自行實作 |
| 資源級授權 (Resource-based) | 不支援 | 支援(依路由參數) |
| 自訂拒絕回應 | 導向登入頁 | 可回傳 JSON、View、403 |
| DI 整合 | 需自訂 AuthorizeAttribute | 原生支援 |
ResourceAuthorizeAttribute<TResource> 泛型類別,減少重複程式碼。bool IsAuthorized(string userName, TResource resource),支援單元測試。GlobalFilters.Filters.Add(new BookAuthorizeAttribute()); 配合 [AllowAnonymous] 例外。ActionFilter 是 MVC 管線中的強大攔截點。將橫切關注點(授權、日誌、快取、例外處理)封裝為 Filter,能讓 Action 專注於業務邏輯,符合單一職責原則。下一篇將探討 ActionMethodSelectorAttribute 實現多角色對應不同 View 的進階選擇機制。