在 ASP.Net Core 中应用基于资源的授权的记录方法是注册一个
AuthorizationHandler
,定义每个 OperationAuthorizationRequirement
,然后使用注入的 AuthorizeAsync()
的 IAuthorizationHandler
方法检查对资源的访问。 (参考文档)
这对于检查单个记录的操作来说非常好,但我的问题是如何最好地同时对许多资源进行授权(例如,根据索引页的记录列表检查读取权限)?
假设我们有一个订单列表,我们希望向用户提供他们已创建的订单列表。要按照 Microsoft 文档定义的实践来做到这一点,我们首先创建一些静态
OperationAuthorizationRequirement
对象:
public static class CrudOperations
{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement { Name = nameof(Create) };
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement { Name = nameof(Read) };
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement { Name = nameof(Update) };
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement { Name = nameof(Delete) };
}
..然后创建我们的 AuthorizationHandler:
public class OrderCreatorAuthorizationHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
InspectionManagementUser resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (resource.CreatedById == currentUserId
&& requirement.Name == CrudOperations.Read.Name) {
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
这已在
Startup.cs
中注册为服务,并且已准备就绪。在我们的视图逻辑中,我们可以使用新的处理程序来获取已过滤的订单列表,如下所示:
//_context is an injected instance of the application's DatabaseContext
//_authorizationService is an injected instance of IAuthorizationService
var allOrders = await _context.Orders.ToListAsync();
var filteredOrders = allOrders
.Where(o => _authorizationService.AuthorizeAsync(User, o, CrudOperations.Read).Result.Succeeded);
这会工作得很好,但对我来说,计算量似乎非常昂贵,因为每条记录都是在内存中单独检查的。随着授权处理程序的逻辑变得更加复杂(例如,如果它涉及数据库调用),这种情况会进一步增加。
让数据库引擎为我们过滤列表可能会更有效,如下所示:
var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var filteredOrders = await _context.Orders
.Where(o => o.CreatedById == currentUserId)
.ToListAsync();
这会执行得更快,但我们现在已经完全绕过了授权逻辑。如果我们稍后决定更改
AuthorizationHandler
中的限制,我们还必须记住在此处以及我们使用此方法的其他任何地方进行更改。如果你问我,这似乎违背了首先分离此授权代码的目的。
对于我缺少的这个问题有一个巧妙的解决方案吗?任何有关最佳实践的建议或指导将不胜感激。
基于资源的授权处理程序在设计时并未考虑您的用例。当然,在数据库查询上添加where子句效率更高,并且还允许将分页操作转发到数据库引擎。如果您可以在数据库的行级别处理授权,则不必担心 AuthorizationHandler。
该功能的设计考虑到了“外部”授权的真实来源。这样可以轻松实现 RBAC 等功能,并将角色成员资格委托给未存储在应用程序 RDBMS 中的单独子系统。 Microsoft Identity 和 Active Directory 就是明显的例子。另一个例子是当您实现微服务架构并且您的服务将授权委托给第三方时。 关于实现,最好的方法是使用
IAsyncEnumerables
和
System.Linq.Async
:public class OrderController : ControllerBase
{
[HttpGet("/orders/{manufacturer}")]
public async Task<IActionResult> ExecuteAsync(
string manufacturer,
CancellationToken cancellation)
{
var source = database.Orders.AsNoTracking()
.Where(order => order.Manufacturer == manufacturer)
.ToAsyncEnumerable();
var results = await GetAuthorizedOrders(source, cancellation)
.Skip(10).Take(10).ToArrayAsync(cancellation)
.ConfigureAwait(false);
return Ok(results);
}
private async IAsyncEnumerable<Order> GetAuthorizedOrders(
IAsyncEnumerable<Order> source,
CancellationToken cancellation)
{
await foreach (var item in source)
{
if (cancellation.IsCancellationRequested) { break; }
var result = await authorizationService
.AuthorizeAsync(User, item, DelegationRequirement.Default)
.ConfigureAwait(false);
if (result.Succeeded) { yield return item; }
}
}
}
public class DelegationHandler : AuthorizationHandler<DelegationRequirement, Order>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DelegationRequirement requirement,
Order resource)
{
// this can do fancy stuff like cache, retry, etc ...
var delegations = await client.GetDelegationsAsync(context.User.Identity.Name);
var has_delegation = delegations
.Where(deleg => deleg.Username == resource.CreatedBy)
.Where(deleg => deleg.Since <= resource.CreatedOn)
.Where(deleg => deleg.Until >= resource.CreatedOn)
.Where(deleg => deleg.MaxValue >= resource.Value)
.Any();
if (has_delegation) { context.Succeed(requirement); }
}
}
当将此方法与两个独立的基础设施用于业务数据和授权时,好处是您可以使用单独的代码来访问每个基础设施。因此,如果由于新的限制而需要发展,您可以更改代码,而不必担心破坏其他部分。