Bài viết này sẽ thảo luận về một số tùy chọn để hạn chế truy cập vào một action method cụ thể – hoặc tất cả các action method – trên một controller ASP.NET Core MVC. Đây không phải là bài viết tổng quát về bảo mật trong ASP.NET Core, mà chỉ tập trung vào khía cạnh này.
Mục lục
Điều Kiện Tiên Quyết
Chúng ta luôn phải thêm hỗ trợ authentication (xác thực – chúng ta là ai) và authorization (ủy quyền – chúng ta có thể làm gì) vào pipeline ASP.NET Core. Authorization yêu cầu authentication, nhưng authentication có thể tồn tại độc lập, miễn là có sơ đồ xác thực được cung cấp:
builder.Services.AddAuthentication()<br>
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);<br>
builder.Services.AddAuthorization(); //có thể nhận các tùy chọn như chúng ta sẽ thấy sau
Sử Dụng Filters
ASP.NET Core filters được biết đến rộng rãi và phổ biến, chúng có lẽ là cách dễ nhất để hạn chế truy cập vào một endpoint. Filters có thể được áp dụng:
- Thông qua một attribute, cho một action method hoặc controller
- Toàn cục cho tất cả controllers và actions
Custom Filters
IAuthorizationFilter hoặc IAsyncAuthorizationFilter là các interface định nghĩa các quy tắc ủy quyền cho một endpoint cụ thể; phiên bản sau là phiên bản bất đồng bộ của phiên bản trước, nhưng chúng tương đương nhau. Chúng có thể được triển khai bởi một attribute, sau đó có thể được áp dụng cho một action method hoặc class controller, hoặc có thể được áp dụng toàn cục cho tất cả các request; điều này dành cho kiểm soát truy cập tùy chỉnh.
Một ví dụ hạn chế truy cập vào một ngày cụ thể trong tuần:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]<br>
public class DayOfWeekFilterAttribute(params DayOfWeek [] daysOfWeek) : Attribute, IAsyncAuthorizationFilter<br>
{<br>
public Task OnAuthorizationAsync(AuthorizationFilterContext context)<br>
{<br>
if (!(daysOfWeek ?? []).Contains(DateTime.Today.DayOfWeek))<br>
{<br>
context.Result = new ForbidResult();<br>
}<br>
}<br>
}
Ý tưởng là: nếu chúng ta muốn trả về thứ gì đó từ chối truy cập, chúng ta có thể làm như vậy từ thuộc tính Result của AuthorizationFilterContext. Các loại kết quả có thể bao gồm:
- ForbidResult: để trả về mã trạng thái HTTP 401 Unauthorized
- RedirectResult, RedirectToActionResult, RedirectToPageResult, RedirectToRouteResult, LocalRedirectResult: các loại chuyển hướng khác nhau (mã trạng thái 3xx)
- EmptyResult: để trả về không có gì với 200 OK
- StatusCodeResult: để trả về mã trạng thái tùy chỉnh
Chúng ta áp dụng nó như sau:
[DaysOfWeek(DayOfWeek.Saturday, DayOfWeek.Sunday)]<br>
public IActionResult Index() { ... }
Authorize Attribute
Ngoài ra còn có attribute [Authorize] tích hợp sẵn có thể được áp dụng cho các class controller hoặc action method, nhưng không triển khai bất kỳ interface nào trong số này. Nó cho phép một số hạn chế:
- Policy: hạn chế bằng một policy có tên, phải được định nghĩa
- Roles: một hoặc nhiều role, phân cách bằng dấu phẩy. Nếu được cung cấp, người dùng hiện tại sẽ cần phải có một trong các role đã cho; roles đến từ các claim của request
Bây giờ, có thể có nhiều attribute [Authorize], và sự khác biệt là:
- Nếu một [Authorize] có nhiều Roles được chỉ định, người dùng cần có một trong số chúng
- Nếu có nhiều [Authorize] với roles, tất cả các attribute [Authorize] phải được kiểm tra, ví dụ: nếu một attribute yêu cầu role “A“, và attribute khác yêu cầu role “B“, thì người dùng hiện tại phải có cả hai role “A” và “B“
- Tương tự cho policies: nếu hai attribute [Authorize] có mặt, cả hai đều có Policy được định nghĩa, giả sử “X” và “Z“, thì người dùng hiện tại phải khớp cả hai policies
Attribute [AllowAnonymous], nếu có mặt, sẽ bỏ qua bất kỳ attribute [Authorize] nào. Dưới đây là một vài ví dụ:
[Authorize(Roles = "Admin")] public class AdminController : Controller //tất cả actions yêu cầu role "Admin"<br>
{<br>
[AllowAnonymous]<br>
public IActionResult SignOut() { ... } //có thể được gọi bởi mọi người<br>
<br>
public IActionResult Index() { ... } //yêu cầu role "Admin" (kế thừa từ controller)<br>
}
public class HomeController : Controller<br>
{<br>
[Authorize]<br>
public IActionResult Private() { ... } //chỉ có thể được gọi bởi người dùng đã xác thực, bất kể roles của họ<br>
<br>
[Authorize(Roles = "Restricted")]<br>
[Authorize(Roles = "Manager,Admin")]<br>
public IActionResult Restricted() { ... } //yêu cầu role "Restricted" VÀ một trong "Manager" hoặc "Admin"<br>
}
Bây giờ, nếu chúng ta muốn sử dụng các policies có tên, chúng ta cần định nghĩa chúng:
builder.Services.AddAuthorization(static options =><br>
{<br>
options.AddPolicy("AdminPolicy", policy =><br>
{<br>
policy.RequireRole("Admin");<br>
policy.RequireAuthenticatedUser();<br>
});<br>
});
Và bây giờ chúng ta có thể sử dụng policy “AdminPolicy” trên một attribute [Authorize]:
[Authorize(Policy = "AdminPolicy")]<br>
public IActionResult Restricted() { ... } //yêu cầu những gì được chỉ định trên "AdminPolicy"
Roles và Policies
Roles và policies là hai cách khác nhau để kiểm soát truy cập. Roles ánh xạ trực tiếp đến authentication claims hoặc đến các nhóm người dùng, tùy thuộc vào loại xác thực đang sử dụng. Policies mặt khác cho phép các yêu cầu có thể tùy chỉnh, có thể bao gồm roles, nhưng nhiều hơn thế nữa.
Bên trong định nghĩa policy chúng ta có thể:
- Yêu cầu người dùng phải được xác thực: RequireAuthenticatedUser()
- Yêu cầu một claim cụ thể (một role là một ví dụ): RequireClaim()
- Yêu cầu một trong danh sách các role: RequireRole()
- Yêu cầu tên người dùng cụ thể: RequireUserName()
- Yêu cầu điều kiện tùy chỉnh dựa trên người dùng hiện tại: RequireAssertion()
- Kết hợp nhiều policies với nhau: Combine()
- Thêm các yêu cầu (thêm về điều này sau): AddRequirements()
Tên policy phải là duy nhất và có thể được sử dụng ở các nơi khác nhau liên quan đến ủy quyền.
Một ví dụ chỉ cho phép truy cập vào cuối tuần:
builder.Services.AddAuthorization(static options =><br>
{<br>
options.AddPolicy("WeekendPolicy", policy =><br>
{<br>
policy.RequireAssertion(ctx =><br>
{<br>
return DateTime.Today.DayOfWeek == DayOfWeek.Saturday || <br>
DateTime.Today.DayOfWeek == DayOfWeek.Sunday;<br>
});<br>
});<br>
});
Cũng có thể có kiểm soát truy cập có cấu trúc và tái sử dụng nhiều hơn, và đây là những gì chúng ta sẽ xem tiếp theo.
Sử Dụng Authorization Handlers
Một authorization handler là một triển khai của IAuthorizationHandler, được triển khai bởi class trừu tượng AuthorizationHandler<TRequirement>, nhận một requirement làm tham số. Bên trong phương thức HandleRequirementAsync chúng ta có thể triển khai bất kỳ logic nào chúng ta muốn, với requirement được truyền dưới dạng tham số:
public record DayOfWeekRequirement(DayOfWeek DayOfWeek) : IAuthorizationRequirement{}
IAuthorizationRequirement chỉ là một marker interface không định nghĩa bất kỳ phương thức nào, chúng ta có thể thêm các thuộc tính vào triển khai cụ thể của mình nếu chúng ta muốn truyền tham số cho handler. Chúng ta kết nối nó với một class AuthorizationHandler<TRequirement>:
public class DayOfWeekAuthenticationHandler : AuthorizationHandler<DayOfWeekRequirement><br>
{<br>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DayOfWeekRequirement requirement)<br>
{<br>
if (DateTime.Today.DayOfWeek == requirement.DayOfWeek)<br>
{<br>
context.Succeed(requirement);<br>
}<br>
else<br>
{<br>
context.Fail(new AuthorizationFailureReason(this, "Wrong day of week"));<br>
}<br>
return Task.CompletedTask;<br>
}<br>
}
Bên trong HandleRequirementAsync chúng ta hoặc thất bại (Fail) hoặc thành công (Succeed) request. Nếu nhiều handlers được đăng ký, tất cả phải thành công để quyền truy cập endpoint được cấp. Cùng một instance của tham số AuthorizationHandlerContext được gọi cho tất cả handlers, và từ đó người ta có thể kiểm tra người dùng đã xác thực hiện tại (User) cũng như ngữ cảnh (Resource), trạng thái hiện tại của request (HasFailed, HasSucceeded), các yêu cầu đã xử lý (Requirements) và các yêu cầu đang chờ xử lý (PendingRequirements), và lý do thất bại (FailureReasons).
Như bạn có thể thấy, đây là một ví dụ đơn giản chỉ lấy một ngày trong tuần được truyền dưới dạng tham số, nhưng có vô số tùy chọn:
- Hạn chế theo địa chỉ IP của client
- Hạn chế theo một số cookie có mặt/không có mặt trên request
- Hạn chế theo một số request header
- …
Chúng ta phải thêm các requirements của chúng ta – bao nhiêu tùy ý – vào một policy có tên:
builder.Services.AddAuthorization(static options =><br>
{<br>
options.AddPolicy("DayOfWeekPolicy", policy =><br>
{<br>
policy.Requirements.Add(new DayOfWeekRequirement(DayOfWeek.Saturday)));<br>
policy.Requirements.Add(new DayOfWeekRequirement(DayOfWeek.Sunday)));<br>
});<br>
});
ASP.NET Core định vị handler thích hợp từ Dependency Injection (DI) bằng cách kiểm tra requirement được truyền. Đừng quên rằng chúng ta phải đăng ký các handlers với DI:
builder.Services.AddSingleton<IAuthorizationHandler, DayOfWeekAuthenticationHandler>();
Với điều này, bây giờ chúng ta có thể thêm một attribute [Authorize] tham chiếu đến policy mới:
[Authorize(Policy = "DayOfWeekPolicy")]<br>
public IActionResult Index() { ... }
So Sánh Các Phương Pháp Thay Thế
Với [Authorize] chúng ta chỉ có thể sử dụng policies hoặc roles trực tiếp. Chỉ định một policy cho phép linh hoạt hơn nhiều, bởi vì chúng ta có thể chỉ định chính xác những gì bạn muốn một cách tách rời, mà bạn có thể thay đổi bất cứ khi nào bạn muốn. Vẫn về điều này, có các authorization handlers là một giải pháp linh hoạt và tái sử dụng hơn, cho phép chúng ta đóng gói nhiều điều kiện với các tham số, với lợi ích bổ sung là có thể sử dụng DI.
Kết Luận
Và đó là tất cả cho bây giờ. Như mọi khi, hy vọng bạn thấy điều này hữu ích!



