MVC Filters
IAuthorizationFilter, TypeFilter & ServiceFilter trong ASP.NET Core
Giới thiệu IAuthorizationFilter
ASP.NET Core có hai cơ chế authorization khác nhau:
Middleware-Based (Chapter 03)
Chạy ở middleware pipeline, trước khi MVC bắt đầu
xử lý.
Dùng [Authorize(Policy = "X")],
policies, requirements và handlers.
MVC Filter-Based (Chapter 06)
Chạy bên trong MVC filter pipeline, sau routing nhưng trước
action method.
Dùng IAuthorizationFilter
với access đến ActionDescriptor, RouteData.
IAuthorizationFilter
nằm trong namespace Microsoft.AspNetCore.Mvc.Filters.
Nó chạy ở đầu MVC filter pipeline — sau routing nhưng
TRƯỚC action method, model binding và các filter khác.
Interface
Có 2 interface: synchronous và asynchronous.
AuthorizationFilterContext
cung cấp:
- HttpContext — request, response, user claims
- ActionDescriptor — thông tin action đang được gọi
- RouteData — route values (controller, action, id...)
- Filters — danh sách tất cả filters trên action
- Result — set giá trị để short-circuit pipeline
Implementation
Sync — IAuthorizationFilter
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; public class AdminOnlyFilter : IAuthorizationFilter { private readonly ILogger<AdminOnlyFilter> _logger; public AdminOnlyFilter(ILogger<AdminOnlyFilter> logger) { _logger = logger; } public void OnAuthorization(AuthorizationFilterContext context) { var user = context.HttpContext.User; if (!user.Identity?.IsAuthenticated ?? true) { _logger.LogWarning("Unauthorized access attempt"); context.Result = new UnauthorizedResult(); // 401 return; } if (!user.IsInRole("Admin")) { _logger.LogWarning("Forbidden: user {User} is not Admin", user.Identity.Name); context.Result = new ForbidResult(); // 403 return; } // Không set Result → pipeline tiếp tục } }
Async — IAsyncAuthorizationFilter
public class SubscriptionFilter : IAsyncAuthorizationFilter { private readonly ISubscriptionService _subscriptions; public SubscriptionFilter(ISubscriptionService subscriptions) { _subscriptions = subscriptions; } public async Task OnAuthorizationAsync( AuthorizationFilterContext context) { var userId = context.HttpContext.User .FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) { context.Result = new UnauthorizedResult(); return; } // Gọi service async để kiểm tra subscription var isActive = await _subscriptions .IsActiveAsync(userId); if (!isActive) { context.Result = new JsonResult(new { error = "Subscription expired" }) { StatusCode = 403 }; } } }
context.Result, pipeline dừng ngay lập tức.
Action method sẽ KHÔNG được gọi.
Các filter phía sau (Resource Filters, Action Filters) cũng không chạy.
Vị trí trong Filter Pipeline
Authorization Filters chạy đầu tiên trong MVC filter pipeline:
Filters
Filters
Binding
Filters
Method
Full Request Lifecycle
Vị trí của IAuthorizationFilter so với middleware authorization:
TypeFilterAttribute & ServiceFilterAttribute
Filter class với constructor dependencies không thể dùng trực tiếp như
một
[Attribute].
ASP.NET Core cung cấp 2 cách để apply filter với DI support:
TypeFilterAttribute
Tự động resolve dependencies qua DI, không cần đăng ký filter trong container.
// Apply filter lên controller hoặc action [TypeFilter(typeof(AdminOnlyFilter))] public class AdminController : Controller { public IActionResult Dashboard() => View(); } // Hoặc trên từng action public class PostsController : Controller { [TypeFilter(typeof(AdminOnlyFilter))] public IActionResult Delete(int id) => View(); }
TypeFilter còn hỗ trợ truyền Arguments bổ sung:
// Filter nhận cả DI services và arguments public class RoleFilter : IAuthorizationFilter { private readonly string _requiredRole; private readonly ILogger<RoleFilter> _logger; public RoleFilter(string requiredRole, ILogger<RoleFilter> logger) { _requiredRole = requiredRole; // từ Arguments _logger = logger; // từ DI container } public void OnAuthorization(AuthorizationFilterContext context) { if (!context.HttpContext.User.IsInRole(_requiredRole)) { context.Result = new ForbidResult(); } } } // Sử dụng — truyền "Admin" qua Arguments [TypeFilter(typeof(RoleFilter), Arguments = new object[] { "Admin" })] public IActionResult ManageUsers() => View(); // Reuse với role khác [TypeFilter(typeof(RoleFilter), Arguments = new object[] { "Editor" })] public IActionResult EditPost() => View();
ServiceFilterAttribute
Yêu cầu filter phải được đăng ký trong DI container. Lifecycle (Transient/Scoped/Singleton) do container kiểm soát.
// 1. Đăng ký filter trong DI container (Program.cs) builder.Services.AddScoped<SubscriptionFilter>(); // 2. Apply bằng ServiceFilter [ServiceFilter(typeof(SubscriptionFilter))] public class PremiumController : Controller { public IActionResult PremiumContent() => View(); }
So sánh
TypeFilterAttribute
- Không cần đăng ký DI
- Hỗ trợ Arguments bổ sung
- Tạo instance mới mỗi lần
- Resolve DI qua
ObjectFactory - Phù hợp khi cần truyền tham số
ServiceFilterAttribute
- Bắt buộc đăng ký DI
- Không hỗ trợ Arguments
- Lifecycle do DI container quản lý
- Resolve qua
IServiceProvider - Phù hợp khi cần kiểm soát lifetime
Khi nào dùng cách nào?
Ba cách tiếp cận authorization, mỗi cách phù hợp với ngữ cảnh khác nhau:
options.AddPolicy(...)
[TypeFilter(typeof(...))]
DI + [ServiceFilter(typeof(...))]
Ví dụ thực tế
Các pattern phổ biến khi dùng IAuthorizationFilter trong production:
Sử dụng TypeFilter để truyền danh sách IP allowed:
public class IpWhitelistFilter : IAuthorizationFilter { private readonly string[] _allowedIps; private readonly ILogger<IpWhitelistFilter> _logger; public IpWhitelistFilter( string[] allowedIps, // từ Arguments ILogger<IpWhitelistFilter> logger) // từ DI { _allowedIps = allowedIps; _logger = logger; } public void OnAuthorization( AuthorizationFilterContext context) { var remoteIp = context.HttpContext.Connection .RemoteIpAddress?.ToString(); if (remoteIp == null || !_allowedIps.Contains(remoteIp)) { _logger.LogWarning( "Blocked IP: {Ip}", remoteIp); context.Result = new JsonResult(new { error = "IP not allowed" }) { StatusCode = 403 }; } } } // Sử dụng [TypeFilter(typeof(IpWhitelistFilter), Arguments = new object[] { new[] { "10.0.0.1", "10.0.0.2", "192.168.1.100" } })] public class InternalApiController : Controller { }
- TypeFilter tự động inject ILogger từ DI
- Mảng IP được truyền qua Arguments
- Có thể reuse với danh sách IP khác nhau cho từng controller
Sử dụng ServiceFilter vì IFeatureFlagService là scoped service:
public class FeatureFlagFilter : IAuthorizationFilter { private readonly IFeatureFlagService _features; public FeatureFlagFilter( IFeatureFlagService features) { _features = features; } public void OnAuthorization( AuthorizationFilterContext context) { // Lấy feature name từ route data var feature = context.RouteData.Values["feature"] ?.ToString(); if (feature != null && !_features.IsEnabled(feature)) { context.Result = new NotFoundResult(); } } } // Đăng ký (Program.cs) builder.Services.AddScoped<FeatureFlagFilter>(); // Sử dụng [ServiceFilter(typeof(FeatureFlagFilter))] public class BetaController : Controller { // Route: /beta/{feature}/... public IActionResult Index(string feature) => View(); }
- ServiceFilter đảm bảo filter dùng cùng scope với request
- IFeatureFlagService có thể cache per-request
- Filter đọc route data để biết feature nào cần kiểm tra