Autorización basada en recursos en ASP.NET Core

En este artículo se describe cómo autorizar a los usuarios el acceso a los recursos de la aplicación.

En una aplicación, un recurso se representa normalmente mediante una clase de C# que incluye los datos almacenados en una colección, como una byte[] matriz. La clase normalmente contiene metadatos adicionales relacionados con el recurso, como un identificador de recurso único, fechas, autores, información de origen y un nombre descriptivo para mostrar en una interfaz de usuario. La colección que contiene datos de recursos normalmente se carga desde contenido de archivo físico, un objeto de almacenamiento en la nube, un objeto en memoria o datos de una base de datos.

La autorización basada en recursos requiere especial atención en ASP.NET Core aplicaciones. La evaluación de atributos se produce antes del enlace de datos y antes de la ejecución de cualquier método que cargue un recurso. La autorización declarativa con un [Authorize] atributo no es suficiente para la autorización basada en recursos. En su lugar, la aplicación debe invocar un método de autorización personalizado, un enfoque conocido como autorización imperativa.

En este artículo se usan ejemplos de componentes de Razor y se centra en escenarios de autorización de Blazor para ASP.NET Core 3.1 o posterior. Para obtener instrucciones de Razor Pages y MVC, que se aplican a todas las versiones de ASP.NET Core, consulte los siguientes recursos:

En los ejemplos de este artículo se usan constructores principales, disponibles en C# 12 (.NET 8) o versiones posteriores. Para obtener más información, vea Declarar constructores principales para clases y estructuras (tutorial de documentación de C#) y constructores principales (Guía de C#).

Aplicación de ejemplo

El Blazor Web App ejemplo de este artículo es la BlazorWebAppAuthorization aplicación de ejemplo (dotnet/AspNetCore.Docs.Samples repositorio de GitHub) (cómo descargar). La aplicación de ejemplo usa cuentas predefinidas con objetos de documento preconfigurados para demostrar los ejemplos de este artículo. Para obtener más información, consulte el archivo LÉAME del ejemplo (README.md).

Caution

Esta aplicación de ejemplo usa una base de datos en memoria para almacenar información de usuario, que no es adecuada para escenarios de producción. La aplicación de ejemplo está pensada solo para fines de demostración y no debe usarse como punto de partida para las aplicaciones de producción.

Uso de la autorización imperativa

La autorización se implementa como una IAuthorizationService, que se registra en la colección de servicios al iniciar la aplicación por el marco de ASP.NET Core. El servicio se pone a disposición de los componentes Razor y otras clases a través de la inyección de dependencias:

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

IAuthorizationService tiene dos sobrecargas del método AuthorizeAsync. Una de las sobrecargas acepta un recurso y un nombre de directiva:

Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource, 
    string policyName);

La otra sobrecarga acepta un recurso y una colección de requisitos (IAuthorizationRequirement) para evaluar:

Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource,
    IEnumerable<IAuthorizationRequirement> requirements);

En el ejemplo siguiente, que se explica completamente en la sección Creación de un controlador basado en recursos, el recurso protegido se carga en un objeto personalizado Document . Se invoca una sobrecarga de AuthorizeAsync para determinar si se le permite al usuario actual acceder al documento en función de la política de autorización "SameAuthorPolicy". Si authorizationResult.Succeeded es true, el usuario tiene autorización para el documento porque es el autor del documento (Document.Author coincide con el Name del usuario):

protected override async Task OnParametersSetAsync()
{
    var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

    if (user.Identity is not null && user.Identity.IsAuthenticated)
    {
        var document = DocumentRepository.Find(DocumentId);

        ...

        var authorizationResult = await AuthorizationService
            .AuthorizeAsync(user, document, "SameAuthorPolicy");

        ...
    }
}

Creación de un controlador basado en recursos

Crear un controlador de autorización basado en recursos es similar a crear un controlador de requisitos sin formato. Cree una clase de requisito personalizada e implemente una clase de controlador de requisitos. Para obtener más información sobre cómo crear una clase de requisitos, consulte Autorización basada en directivas: Requisitos.

Se usa la siguiente clase de demostración Document :

namespace BlazorWebAppAuthorization.Models;

public class Document
{
    public string? Author { get; set; }

    public byte[]? Content { get; set; }

    public Guid ID { get; set; }

    public string? Title { get; set; }
}

La clase de controlador especifica el requisito y el tipo de recurso. En el ejemplo siguiente se muestra un controlador que usa un SameAuthorRequirement requisito y un Document recurso.

Services/DocumentAuthorizationHandler.cs:

using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Services;

public class DocumentAuthorizationHandler :
    AuthorizationHandler<SameAuthorRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        SameAuthorRequirement requirement, 
        Document resource)
    {
        if (context.User.Identity?.Name == resource.Author)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }

Registre el requisito y el controlador en Program.cs:

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));

builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

Registre el requisito y el controlador en Startup.ConfigureServices:

services.AddAuthorization(options =>
{
    options.AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));
});

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

Para obtener más información sobre la creación de directivas de autorización, vea Autorización basada en directivas en ASP.NET Core.

El componente siguiente AccessDocument llama a una sobrecarga de AuthorizeAsync para determinar si el usuario actual tiene permiso para ver un documento en función de la directiva de autorización "SameAuthorPolicy". Si authorizationResult.Succeeded es true, el usuario está autorizado respecto al documento porque es el autor del documento (Document.Author coincide con el Name del usuario).

Pages/AccessDocument.razor:

@page "/access-document/{documentId}"
@using Microsoft.AspNetCore.Authorization
@using BlazorWebAppAuthorization.Data
@inject AuthenticationStateProvider AuthStateProvider
@inject IAuthorizationService AuthorizationService
@inject IDocumentRepository DocumentRepository

<h1>Access Document</h1>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
        <p>@message</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized to access this page.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    private string? message;

    [Parameter]
    public string? DocumentId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            var document = DocumentRepository.Find(DocumentId);

            if (document == null)
            {
                message = "Document not found.";
                return;
            }

            var authorizationResult = await AuthorizationService
                .AuthorizeAsync(user, document, "SameAuthorPolicy");

            message = authorizationResult.Succeeded
                ? $"You are authorized for document {DocumentId}."
                : $"You are NOT authorized for document {DocumentId}.";
        }
    }
}

En la aplicación de ejemplo, cada usuario de la aplicación tiene acceso autorizado al documento inicializado que creó.

Requisitos operativos

Para tomar decisiones basadas en los resultados de las operaciones CRUD (Crear, Leer, Actualizar, Eliminar), use la OperationAuthorizationRequirement clase auxiliar. La clase auxiliar permite escribir un único controlador en lugar de una clase individual para cada tipo de operación. La siguiente Operations clase establece los cuatro tipos de operación CRUD:

using Microsoft.AspNetCore.Authorization.Infrastructure;

public static class Operations
{
    public static readonly OperationAuthorizationRequirement Create =
        new() { Name = nameof(Create) };
    public static readonly OperationAuthorizationRequirement Delete =
        new() { Name = nameof(Delete) };
    public static readonly OperationAuthorizationRequirement Read =
        new() { Name = nameof(Read) };
    public static readonly OperationAuthorizationRequirement Update =
        new() { Name = nameof(Update) };
}

El siguiente DocumentAuthorizationCrudHandler controlador de autorización valida la operación mediante el recurso, la identidad (rol) del usuario en algunos casos y la propiedad del Name requisito:

  • Todos los usuarios pueden leer documentos.
  • Solo los usuarios del Admin rol pueden crear y actualizar documentos.
  • Solo los usuarios del SuperUser rol pueden eliminar documentos.

Services/DocumentAuthorizationCrudHandler.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Services;

public class DocumentAuthorizationCrudHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement, 
        Document resource)
    {
        if (requirement.Name == Operations.Create.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Delete.Name &&
            context.User.IsInRole("SuperUser"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Read.Name)
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Update.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Dónde se registran los servicios en la aplicación:

builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationCrudHandler>();

Llame a la sobrecarga de AuthorizeAsync con la operación para devolver el resultado de la autorización.

Para la autorización para crear un documento:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Create);

Para tener autorización para leer un documento:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Read);

Para autorizar la acción de eliminar un documento:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Delete);

Para autorizar la actualización de un documento:

var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Update);

En la página AccessDocumentCrud de la aplicación de ejemplo:

  • Leela (leela@contoso.com), en calidad de Admin y SuperUser, puede realizar operaciones CRUD completas sobre los recursos.
  • Harry (harry@contoso.com), como solo un Admin, puede crear, leer y actualizar recursos.
  • Sarah (sarah@contoso.com), como única SuperUser, puede eliminar y leer recursos.