使用 ASP.NET Core 自定义端点元数据实现细粒度访问控制

作者:微信公众号:【架构师老卢】
3-17 14:37
25

概述:终结点元数据是与 ASP.NET Core 应用程序中的每个终结点关联的信息片段。终结点本质上是 Web 应用程序的入口点,例如 MVC 控制器操作或最小 API 中的路由,可以处理 HTTP 请求。端点元数据允许描述这些端点的特征和行为,例如授权策略、CORS(跨域资源共享)限制、筛选器等。在 MVC 控制器或 API 控制器的上下文中,终结点元数据通常使用属性定义,例如 或 。随着 ASP.NET Core 6 及更高版本中引入最少 API,端点元数据的概念也已扩展到这些更轻量级、更灵活的模型。终结点元数据通常使用 Fluent 方法添加,例如 或 。[Authorize][Produce

终结点元数据是与 ASP.NET Core 应用程序中的每个终结点关联的信息片段。终结点本质上是 Web 应用程序的入口点,例如 MVC 控制器操作或最小 API 中的路由,可以处理 HTTP 请求。端点元数据允许描述这些端点的特征和行为,例如授权策略、CORS(跨域资源共享)限制、筛选器等。

在 MVC 控制器或 API 控制器的上下文中,终结点元数据通常使用属性定义,例如 或 。随着 ASP.NET Core 6 及更高版本中引入最少 API,端点元数据的概念也已扩展到这些更轻量级、更灵活的模型。终结点元数据通常使用 Fluent 方法添加,例如 或 。[Authorize][Produces]RequireAuthorization()RequireRateLimiting()

在本文中,我们将探讨如何使用自己的端点元数据,并在授权策略中使用它们,以便在 ASP.NET Core 应用程序中实现细粒度访问控制。

创建自己的终结点元数据

我们将基于细粒度权限系统实施授权策略。您将在以下各节中看到的代码受 Microsoft.Identity.Web 库的启发,该库使用这些相同的原则来实现基于 OAuth 2.0 范围的授权。

让我们从细粒度的权限系统开始。我们将定义一个简单的枚举来表示可以授予的不同权限:

public enum Permission  
{  
    Read,  
    Write,  
    // ...  
}

接下来,我们将创建一个表示端点元数据的属性,允许端点指定一个或多个必需的权限:

public interface IRequiredPermissionMetadata
{
    HashSet<Permission> RequiredPermissions { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequiredPermissionAttribute(Permission requiredPermission, params Permission[] additionalRequiredPermissions)
    : Attribute, IRequiredPermissionMetadata
{
    public HashSet<Permission> RequiredPermissions { get; } = [requiredPermission, ..additionalRequiredPermissions];
}

该属性可用于控制器方法和类,如下所示:[RequiredPermission]

[ApiController]
[Route("[controller]")]
public class HelloController : ControllerBase
{
    [HttpGet]
    [RequiredPermission(Permission.Read, Permission.Write)]
    public IActionResult Index()
    {
        return this.Ok("Hello world");
    }
}

如果要将其添加为最小 API 上的终结点元数据,可以创建以下扩展方法:

public static TBuilder RequirePermission<TBuilder>(
    this TBuilder endpointConventionBuilder, Permission requiredPermission, params Permission[] additionalRequiredPermissions)
    where TBuilder : IEndpointConventionBuilder
{
    return endpointConventionBuilder.WithMetadata(new RequiredPermissionAttribute(requiredPermission, additionalRequiredPermissions));
}

因此,您可以通过以下方式使用它:

app.MapGet("/", () => "Hello World!")  
    .RequirePermission(Permission.Read);

在自定义授权要求中使用端点元数据

现在,我们的端点已使用元数据进行修饰,我们可以创建授权要求。在 ASP.NET Core 中,授权策略是授权要求的集合,允许您编写精细规则来控制对资源的访问。

让我们为我们的权限系统创建一个要求:

public class PermissionAuthorizationRequirement : IAuthorizationRequirement;

我们的授权要求不需要包含数据,因为我们将使用授权处理程序来检索在用户尝试访问的终结点上声明的权限。

public sealed class RequiredPermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
    {
        var endpoint = context.Resource switch
        {
            HttpContext httpContext => httpContext.GetEndpoint(),
            Endpoint ep => ep,
            _ => null,
        };

        var requiredPermissions = endpoint?.Metadata.GetMetadata<IRequiredPermissionMetadata>()?.RequiredPermissions;
        if (requiredPermissions == null)
        {
            // The endpoint is not decorated with the required permission metadata
            return Task.CompletedTask;
        }

        // TODO: Implement your custom logic to check if the user has the required permissions
        var hasRequiredPermissions = true;

        if (hasRequiredPermissions)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

请注意,我们通过提供端点元数据接口来使用该方法。此方法可确保检索实现接口的元数据,并使用内部缓存进行优化。EndpointMetadataCollection.GetMetadata<T>IRequiredPermissionMetadata

现在,剩下的就是在应用程序的服务中声明我们的授权策略。

注册授权策略

让我们创建一个虚构的授权策略,该策略要求使用元数据修饰受此策略保护的终结点,并使用身份验证方案对用户进行身份验证。我们的政策将被命名为 .RequiredPermissionAttributeCookiesRequiredPermissions

public static class RequiredPermissionDefaults  
{  
    public const string PolicyName = "RequiredPermission";  
}  
  
public static class RequiredPermissionAuthorizationExtensions  
{  
    public static AuthorizationPolicyBuilder RequireRequiredPermissions(this AuthorizationPolicyBuilder builder)  
    {  
        return builder.AddRequirements(new PermissionAuthorizationRequirement());  
    }  
  
    public static AuthorizationBuilder AddRequiredPermissionPolicy(this AuthorizationBuilder builder)  
    {  
        builder.AddPolicy(RequiredPermissionDefaults.PolicyName, policy =>  
        {  
            policy.RequireAuthenticatedUser();  
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);  
              
            policy.RequireRequiredPermissions();  
        });  
  
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthorizationHandler, RequiredPermissionAuthorizationHandler>());  
  
        return builder;  
    }  
}

在上面的代码中,我们借此机会将我们的实现注册到应用程序的服务中。这将允许我们的处理程序由 ASP.NET Core 的依赖注入系统解析。RequiredPermissionAuthorizationHandler

最后,我们可以在我们的服务中注册我们的政策:

var builder = WebApplication.CreateBuilder(args);  
  
// **TODO:** Register your authentication scheme!  
builder.Services.AddAuthorization();  
  
builder.Services.AddAuthorizationBuilder()  
    .AddRequiredPermissionPolicy();  
  
builder.Services.AddAuthorization(options =>  
{  
    options.FallbackPolicy = options.GetPolicy(RequiredPermissionDefaults.PolicyName);  
});  
  
var app = builder.Build();  
  
app.UseAuthentication();  
app.UseAuthorization();  
  
app.MapGet("/", () => "Hello World!")  
    .RequirePermission(Permission.Read);  
  
app.Run();

此设置可确保我们的应用程序将策略用作回退策略,将我们的细粒度访问控制机制应用于尚未受其他策略保护或匿名的所有终结点。RequiredPermissions

结论

在本文中,我们探讨了如何使用终结点元数据在 ASP.NET Core 应用程序中实现精细权限系统。我们创建了自己的元数据,并在授权策略中使用了它,以控制对资源的访问。我们还了解了如何使用授权处理程序从端点元数据中检索所需的权限。RequiredPermissionAttribute

你负责在授权处理程序中实现用于验证权限的逻辑。这同样适用于要使用的授权策略和身份验证方案。

相关留言评论
昵称:
邮箱:
阅读排行