Autorizzazione basata sulle risorse in ASP.NET Core

Questo articolo descrive come autorizzare gli utenti per l'accesso alle risorse dell'app.

In un'app una risorsa è in genere rappresentata da una classe C# che include i dati archiviati in una raccolta, ad esempio una byte[] matrice. La classe contiene in genere metadati aggiuntivi relativi alla risorsa, ad esempio un identificatore di risorsa univoco, date, autori, informazioni di origine e un nome descrittivo per la visualizzazione in un'interfaccia utente. La raccolta che contiene i dati delle risorse viene in genere caricata dal contenuto del file fisico, da un oggetto di archiviazione cloud, da un oggetto in memoria o da un database.

L'autorizzazione basata sulle risorse richiede particolare attenzione nelle app ASP.NET Core. La valutazione degli attributi viene eseguita prima del data binding e prima dell'esecuzione di qualsiasi metodo che carica una risorsa. L'autorizzazione dichiarativa con un [Authorize] attributo non è sufficiente per l'autorizzazione basata su risorse. L'app deve invece richiamare un metodo di autorizzazione personalizzato, un approccio noto come autorizzazione imperativa.

Questo articolo usa esempi di componenti Razor e si concentra sugli scenari di autorizzazione Blazor per ASP.NET Core 3.1 o versione successiva. Per indicazioni su Razor Pages e MVC, applicabili a tutte le versioni di ASP.NET Core, vedere le risorse seguenti:

Gli esempi in questo articolo usano costruttori primari, disponibili in C# 12 (.NET 8) o versioni successive. Per altre informazioni, vedere Dichiara costruttori primari per classi e struct (esercitazione sulla documentazione di C#) e Costruttori primari (Guida di C#).

Esempio di app

L'esempio Blazor Web App per questo articolo è l'app di esempio BlazorWebAppAuthorization (repository dotnet/AspNetCore.Docs.Samples GitHub) (come scaricare). L'app di esempio utilizza account prepopolati con documenti preconfigurati per illustrare gli esempi in questo articolo. Per altre informazioni, vedere il file README dell'esempio (README.md).

Caution

Questa app di esempio usa un database in memoria per archiviare le informazioni utente, che non sono adatte per gli scenari di produzione. L'app di esempio è destinata solo a scopi dimostrativi e non deve essere usata come punto di partenza per le app di produzione.

Usare l'autorizzazione imperativa

L'autorizzazione viene implementata come un IAuthorizationService, che viene registrato nella raccolta di servizi all'avvio dell'app dal framework ASP.NET Core. Il servizio viene reso disponibile ai Razor componenti e ad altre classi tramite inserimento delle dipendenze:

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

IAuthorizationService ha due AuthorizeAsync sovraccarichi del metodo. Uno dei sovraccarichi accetta una risorsa e un nome di criterio:

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

L'altro sovraccarico accetta una risorsa e una raccolta di requisiti (IAuthorizationRequirement) da valutare:

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

Nell'esempio seguente, descritto in modo completo nella sezione Creare un gestore basato su risorse , la risorsa protetta viene caricata in un oggetto personalizzato Document . Viene chiamato un sovraccarico di AuthorizeAsync per determinare se all'utente corrente è consentito accedere al documento in base ai criteri di autorizzazione "SameAuthorPolicy". Se authorizationResult.Succeeded è true, l'utente è autorizzato per il documento perché ha creato il documento (Document.Author corrisponde a Name):

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");

        ...
    }
}

Creare un gestore basato su risorse

La creazione di un gestore di autorizzazione basato su risorse è simile alla creazione di un gestore di requisiti semplici. Creare una classe di requisiti personalizzata e implementare una classe del gestore dei requisiti. Per altre informazioni sulla creazione di una classe di requisiti, vedere Autorizzazione basata su criteri: Requisiti.

Viene usata la classe dimostrativa Document seguente:

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 classe del gestore specifica il requisito e il tipo di risorsa. L'esempio seguente illustra un gestore che usa un SameAuthorRequirement requisito e una Document risorsa.

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 { }

Registra il requisito e il gestore in Program.cs:

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

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

Registrate il requisito e il gestore in Startup.ConfigureServices:

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

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

Per ulteriori informazioni sulla creazione di criteri di autorizzazione, vedi Autorizzazione basata su criteri in ASP.NET Core.

Il componente seguente AccessDocument chiama un AuthorizeAsync overload per determinare se l'utente corrente è autorizzato a visualizzare un documento in base ai criteri di autorizzazione "SameAuthorPolicy". Se authorizationResult.Succeeded è true, l'utente è autorizzato per il documento perché ha creato il documento (Document.Author corrisponde all'utente Name).

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}.";
        }
    }
}

Nella app di esempio, ogni utente dell'app è autorizzato ad accedere al documento iniziale che ha creato.

Requisiti operativi

Per prendere decisioni in base ai risultati delle operazioni CRUD (Create, Read, Update, Delete), usare la OperationAuthorizationRequirement classe helper. La classe helper consente di scrivere un singolo gestore anziché una singola classe per ogni tipo di operazione. La classe seguente Operations stabilisce tutti e quattro i tipi di operazione 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) };
}

Il gestore di autorizzazione seguente DocumentAuthorizationCrudHandler convalida l'operazione usando la risorsa, l'identità dell'utente (ruolo) in alcuni casi e la proprietà del Name requisito:

  • Tutti gli utenti possono leggere i documenti.
  • Solo gli utenti nel Admin ruolo possono creare e aggiornare documenti.
  • Solo gli utenti nel SuperUser ruolo possono eliminare documenti.

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;
    }
}

Dove vengono registrati i servizi nell'app:

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

Chiamare l'overload di AuthorizeAsync con l'operazione per restituire il risultato dell'autorizzazione.

Per l'autorizzazione a creare un documento:

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

Per l'autorizzazione a leggere un documento:

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

Per l'autorizzazione all'eliminazione di un documento:

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

Per l'autorizzazione ad aggiornare un documento:

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

Nella pagina dell'app di esempioAccessDocumentCrud:

  • Leela (leela@contoso.com), come Admin e SuperUser, può eseguire operazioni CRUD complete sulle risorse.
  • Harry (harry@contoso.com), in quanto solo Admin, può creare, leggere e aggiornare le risorse.
  • Sarah (sarah@contoso.com), in quanto unica SuperUser, può eliminare e leggere le risorse.