Autorizzazione basata su criteri in ASP.NET Core

Sotto le quinte, l'autorizzazione basata sui ruoli e l'autorizzazione basata sulle attestazioni usano un requisito, un gestore dei requisiti e un criterio preconfigurato. Questi blocchi predefiniti supportano l'espressione delle valutazioni di autorizzazione nel codice. Il risultato è una struttura di autorizzazione più completa, riutilizzabile e testabile.

Un criterio di autorizzazione è costituito da uno o più requisiti. Registrarlo come parte della configurazione del servizio di autorizzazione nel file dell'app Program.cs :

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

Nell'esempio precedente viene creato un criterio "AtLeast21". Ha un singolo requisito, ovvero quello di un'età minima, che viene fornita come parametro per il requisito.

IAuthorizationService

Il servizio primario che determina se l'autorizzazione ha esito positivo è IAuthorizationService:

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

Il codice precedente evidenzia i due metodi di IAuthorizationService.

IAuthorizationRequirement è un servizio marcatore senza metodi e il meccanismo per tenere traccia dell'esito positivo dell'autorizzazione.

Ognuno IAuthorizationHandler è responsabile del controllo se vengono soddisfatti i requisiti:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

La AuthorizationHandlerContext classe è ciò che il gestore usa per contrassegnare se i requisiti sono stati soddisfatti:

 context.Succeed(requirement)

Il codice seguente illustra l'implementazione predefinita semplificata (e annotata con commenti) del servizio di autorizzazione:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

Il codice seguente illustra una configurazione tipica del servizio di autorizzazione:

// Add all of your handlers to DI.
builder.Services.AddSingleton<IAuthorizationHandler, MyHandler1>();
// MyHandler2, ...

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

// Configure your policies
builder.Services.AddAuthorization(options =>
      options.AddPolicy("Something",
      policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));

Usare IAuthorizationService, [Authorize(Policy = "Something")]o RequireAuthorization("Something") per l'autorizzazione.

Applicare criteri ai controller MVC

Per le app che usano Razor Pages, vedere la sezione Applicare criteri alle Razor pagine .

Applicare criteri ai controller usando l'attributo con il [Authorize] nome del criterio:

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}

Se vengono applicati più criteri a livello di controller e azione, tutti i criteri devono superare prima che venga concesso l'accesso:

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}

Applicare criteri alle Razor pagine

Applicare criteri a Razor Pages usando l'attributo [Authorize] con il nome del criterio. Ad esempio:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AuthorizationPoliciesSample.Pages;

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Model : PageModel { }

I criteri non possono essere applicati a livello di Razor gestore pagina, ma devono essere applicati alla pagina.

I criteri possono essere applicati anche alle Razor pagine usando una convenzione di autorizzazione.

Applicare criteri agli endpoint

Applicare criteri agli endpoint usando RequireAuthorization con il nome del criterio. Ad esempio:

app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");

Requisiti

Un requisito di autorizzazione è una raccolta di parametri di dati che un criterio può usare per valutare l'entità utente corrente. Nel criterio "AtLeast21", il requisito è un singolo parametro, ovvero l'età minima. Un requisito implementa IAuthorizationRequirement, che è un'interfaccia del marcatore vuota. Un requisito di validità minima con parametri può essere implementato come segue:

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int minimumAge) =>
        MinimumAge = minimumAge;

    public int MinimumAge { get; }
}

Se un criterio di autorizzazione contiene più requisiti di autorizzazione, tutti i requisiti devono essere passati affinché la valutazione dei criteri abbia esito positivo. In altre parole, più requisiti di autorizzazione aggiunti a un singolo criterio di autorizzazione vengono trattati su base AND .

Nota

Un requisito non deve avere dati o proprietà.

Gestori di autorizzazione

Un gestore di autorizzazione è responsabile della valutazione delle proprietà di un requisito. Il gestore dell'autorizzazione valuta i requisiti rispetto a un oggetto fornito AuthorizationHandlerContext per determinare se l'accesso è consentito.

Un requisito può avere più gestori. Un gestore può ereditare AuthorizationHandler<TRequirement>, dove TRequirement è il requisito da gestire. In alternativa, un gestore può implementare IAuthorizationHandler direttamente per gestire più di un tipo di requisito.

Usare un gestore per un requisito

L'esempio seguente mostra una relazione uno-a-uno in cui un gestore di età minima gestisce un singolo requisito:

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://contoso.com");

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Il codice precedente determina se l'entità utente corrente ha una data di nascita che è stata rilasciata da un emittente noto e attendibile. L'autorizzazione non può verificarsi quando manca l'attestazione, nel qual caso viene restituita un'attività completata. Quando è presente un'attestazione, viene calcolata l'età dell'utente. Se l'utente soddisfa l'età minima definita dal requisito, l'autorizzazione viene considerata corretta. Quando l'autorizzazione ha esito positivo, context.Succeed viene richiamata con il requisito soddisfatto come unico parametro.

Usare un gestore per più requisiti

L'esempio seguente mostra una relazione uno-a-molti in cui un gestore di autorizzazioni può gestire tre diversi tipi di requisiti:

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource)
                    || IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission || requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        return Task.CompletedTask;
    }

    private static bool IsOwner(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }

    private static bool IsSponsor(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }
}

Il codice precedente attraversa una proprietà contenente i requisiti non contrassegnati PendingRequirementscome riusciti. Per un ReadPermission requisito, l'utente deve essere un proprietario o uno sponsor per accedere alla risorsa richiesta. Per un EditPermission requisito o DeletePermission , devono essere proprietari per accedere alla risorsa richiesta.

Registrazione del gestore

Registrare i gestori nella raccolta di servizi durante la configurazione. Ad esempio:

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

Il codice precedente viene MinimumAgeHandler registrato come singleton. I gestori possono essere registrati usando una qualsiasi durata del servizio predefinita.

È possibile aggregare sia un requisito che un gestore in una singola classe che implementa sia IAuthorizationRequirement che IAuthorizationHandler. Questo bundledling crea un accoppiamento stretto tra il gestore e il requisito ed è consigliato solo per requisiti e gestori semplici. La creazione di una classe che implementa entrambe le interfacce rimuove la necessità di registrare il gestore in inserimento di dipendenze a causa del PassThroughAuthorizationHandler predefinito che consente ai requisiti di gestire se stessi.

Per un buon esempio, vedere la classe AssertionRequirement in cui AssertionRequirement è un requisito e il gestore in una classe completamente autonoma.

Cosa deve restituire un gestore?

Si noti che il Handle metodo nell'esempio del gestore non restituisce alcun valore. Come è indicato uno stato di esito positivo o negativo?

  • Un gestore indica l'esito positivo chiamando context.Succeed(IAuthorizationRequirement requirement), passando il requisito che è stato convalidato correttamente.

  • Un gestore non deve gestire gli errori in genere, perché altri gestori per lo stesso requisito possono avere esito positivo.

  • Per garantire un errore, anche se altri gestori dei requisiti hanno esito positivo, chiamare context.Fail.

Se un gestore chiama context.Succeed o context.Fail, tutti gli altri gestori vengono comunque chiamati. Ciò consente ai requisiti di produrre effetti collaterali, ad esempio la registrazione, che avviene anche se un altro gestore ha convalidato o non ha superato un requisito. Se impostata su false, la InvokeHandlersAfterFailure proprietà cortocircuita l'esecuzione dei gestori quando context.Fail viene chiamata. InvokeHandlersAfterFailure il valore predefinito è true, nel qual caso vengono chiamati tutti i gestori.

Nota

I gestori di autorizzazione vengono chiamati anche se l'autenticazione non riesce. Inoltre, i gestori possono essere eseguiti in qualsiasi ordine, quindi non dipendono dal fatto che vengano chiamati in un ordine specifico.

Perché è consigliabile usare più gestori per un requisito?

Nei casi in cui si vuole che la valutazione sia su base OR , implementare più gestori per un singolo requisito. Ad esempio, Microsoft ha porte aperte solo con le schede chiave. Se lasci la carta chiave a casa, il receptionist stampa un adesivo temporaneo e apre la porta per te. In questo scenario si avrà un singolo requisito, BuildingEntry, ma più gestori, ognuno dei quali esamina un singolo requisito.

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

public class BuildingEntryRequirement : IAuthorizationRequirement { }

BadgeEntryHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "BadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "TemporaryBadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            // Code to check expiration date omitted for brevity.
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Assicurarsi che entrambi i gestori siano registrati. Se uno dei due gestori ha esito positivo quando un criterio valuta BuildingEntryRequirement, la valutazione dei criteri ha esito positivo.

Usare un func per soddisfare un criterio

Possono verificarsi situazioni in cui l'adempimento di un criterio è semplice da esprimere nel codice. È possibile fornire un oggetto Func<AuthorizationHandlerContext, bool> quando si configura un criterio con il RequireAssertion generatore di criteri.

Ad esempio, il precedente BadgeEntryHandler può essere riscritto come segue:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context => context.User.HasClaim(c =>
            (c.Type == "BadgeId" || c.Type == "TemporaryBadgeId")
            && c.Issuer == "https://microsoftsecurity")));
});

Accedere al contesto della richiesta MVC nei gestori

Il HandleRequirementAsync metodo ha due parametri: un AuthorizationHandlerContext e l'oggetto TRequirement gestito. Framework come MVC o SignalR sono liberi di aggiungere qualsiasi oggetto alla Resource proprietà su AuthorizationHandlerContext per passare informazioni aggiuntive.

Quando si usa il routing degli endpoint, l'autorizzazione viene in genere gestita dal middleware di autorizzazione. In questo caso, la proprietà è un'istanza Resource di HttpContext. Il contesto può essere usato per accedere all'endpoint corrente, che può essere usato per eseguire il probe della risorsa sottostante a cui si sta eseguendo il routing. Ad esempio:

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

Con il routing tradizionale o quando l'autorizzazione viene eseguita come parte del filtro di autorizzazione di MVC, il valore di Resource è un'istanza AuthorizationFilterContext di . Questa proprietà fornisce l'accesso a HttpContext, RouteDatae tutto il resto fornito da MVC e Razor Pages.

L'uso della proprietà è specifico del Resource framework. L'uso delle informazioni nella proprietà limita i Resource criteri di autorizzazione a framework specifici. Eseguire il cast della proprietà usando la Resourceis parola chiave e quindi verificare che il cast abbia avuto esito positivo per assicurarsi che il codice non si arresti in modo anomalo quando InvalidCastException viene eseguito in altri framework:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

A livello globale, tutti gli utenti devono essere autenticati

Per informazioni su come richiedere a livello globale l'autenticazione di tutti gli utenti, vedere Richiedere utenti autenticati.

Esempio di autorizzazione con servizio esterno

Il codice di esempio in AspNetCore.Docs.Samples illustra come implementare requisiti di autorizzazione aggiuntivi con un servizio di autorizzazione esterno. Il progetto di esempio Contoso.API è protetto con Azure AD. Un controllo di autorizzazione aggiuntivo dal Contoso.Security.API progetto restituisce un payload che descrive se l'app Contoso.API client può richiamare l'API GetWeather .

Configurare l'esempio

  1. Creare una registrazione dell'applicazione nel tenant di Azure Active Directory (Azure AD):
  • Assegnargli un AppRole.
  • In Autorizzazioni API aggiungere AppRole come autorizzazione e concedere Amministrazione consenso. Si noti che in questa configurazione questa registrazione dell'app rappresenta sia l'API che il client che richiamano l'API. Se si preferisce, è possibile creare due registrazioni di app. Se si usa questa configurazione, assicurarsi di eseguire solo le autorizzazioni api, aggiungere AppRole come passaggio di autorizzazione solo per il client. Solo la registrazione dell'app client richiede la generazione di un segreto client.
  1. Configurare il Contoso.API progetto con le impostazioni seguenti:
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<Tenant name from AAD properties>.onmicrosoft.com">,
    "TenantId": "<Tenant Id from AAD properties>",
    "ClientId": "<Client Id from App Registration representing the API>"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
  1. Configurare Contoso.Security.API con le seguenti impostazioni:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "<Use the appropriate Client Id representing the Client calling the API>"
  ]
}
  1. Importare il file ContosoAPI.postman_collection.json in Postman e configurare un ambiente con quanto segue:

    • ClientId: ID client dalla registrazione dell'app che rappresenta il client che chiama l'API.
    • clientSecret: segreto client dalla registrazione dell'app che rappresenta il client che chiama l'API.
    • TenantId: ID tenant dalle proprietà di AAD
  2. Eseguire la soluzione e usare Postman per richiamare l'API. È possibile aggiungere punti di interruzione in Contoso.Security.API.SecurityPolicyController e osservare che l'ID client viene passato che viene usato per asserire se è consentito ottenere meteo.

Risorse aggiuntive

Sotto le quinte, l'autorizzazione basata sui ruoli e l'autorizzazione basata sulle attestazioni usano un requisito, un gestore dei requisiti e un criterio preconfigurato. Questi blocchi predefiniti supportano l'espressione delle valutazioni di autorizzazione nel codice. Il risultato è una struttura di autorizzazione più completa, riutilizzabile e testabile.

Un criterio di autorizzazione è costituito da uno o più requisiti. Viene registrato come parte della configurazione del servizio di autorizzazione, nel Startup.ConfigureServices metodo :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });
}

Nell'esempio precedente viene creato un criterio "AtLeast21". Ha un singolo requisito, ovvero quello di un'età minima, che viene fornita come parametro per il requisito.

IAuthorizationService

Il servizio primario che determina se l'autorizzazione ha esito positivo è IAuthorizationService:

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

Il codice precedente evidenzia i due metodi di IAuthorizationService.

IAuthorizationRequirement è un servizio marcatore senza metodi e il meccanismo per tenere traccia dell'esito positivo dell'autorizzazione.

Ognuno IAuthorizationHandler è responsabile del controllo se vengono soddisfatti i requisiti:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

La AuthorizationHandlerContext classe è ciò che il gestore usa per contrassegnare se i requisiti sono stati soddisfatti:

 context.Succeed(requirement)

Il codice seguente illustra l'implementazione predefinita semplificata (e annotata con commenti) del servizio di autorizzazione:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

Il codice seguente mostra un tipico ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    // Add all of your handlers to DI.
    services.AddSingleton<IAuthorizationHandler, MyHandler1>();
    // MyHandler2, ...

    services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

    // Configure your policies
    services.AddAuthorization(options =>
          options.AddPolicy("Something",
          policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));


    services.AddControllersWithViews();
    services.AddRazorPages();
}

Usare IAuthorizationService o [Authorize(Policy = "Something")] per l'autorizzazione.

Applicare criteri al controller MVC

Se si usa Razor Pages, vedere Applicare criteri alle Razor pagine in questo documento.

I criteri vengono applicati ai controller usando l'attributo [Authorize] con il nome del criterio. Ad esempio:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}

Applicare criteri alle Razor pagine

I criteri vengono applicati alle Razor pagine usando l'attributo [Authorize] con il nome del criterio. Ad esempio:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}

I criteri non possono essere applicati a livello di Razor gestore pagina, ma devono essere applicati alla pagina.

I criteri possono essere applicati alle Razor pagine usando una convenzione di autorizzazione.

Requisiti

Un requisito di autorizzazione è una raccolta di parametri di dati che un criterio può usare per valutare l'entità utente corrente. Nel criterio "AtLeast21", il requisito è un singolo parametro, ovvero l'età minima. Un requisito implementa IAuthorizationRequirement, che è un'interfaccia del marcatore vuota. Un requisito di validità minima con parametri può essere implementato come segue:

using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

Se un criterio di autorizzazione contiene più requisiti di autorizzazione, tutti i requisiti devono essere passati affinché la valutazione dei criteri abbia esito positivo. In altre parole, più requisiti di autorizzazione aggiunti a un singolo criterio di autorizzazione vengono trattati su base AND .

Nota

Un requisito non deve avere dati o proprietà.

Gestori di autorizzazione

Un gestore di autorizzazione è responsabile della valutazione delle proprietà di un requisito. Il gestore dell'autorizzazione valuta i requisiti rispetto a un oggetto fornito AuthorizationHandlerContext per determinare se l'accesso è consentito.

Un requisito può avere più gestori. Un gestore può ereditare AuthorizationHandler<TRequirement>, dove TRequirement è il requisito da gestire. In alternativa, un gestore può implementare IAuthorizationHandler per gestire più di un tipo di requisito.

Usare un gestore per un requisito

L'esempio seguente mostra una relazione uno-a-uno in cui un gestore di età minima utilizza un singolo requisito:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
                                        c.Issuer == "http://contoso.com"))
        {
            //TODO: Use the following if targeting a version of
            //.NET Framework older than 4.6:
            //      return Task.FromResult(0);
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth && 
                                        c.Issuer == "http://contoso.com").Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

Il codice precedente determina se l'entità utente corrente ha una data di nascita che è stata rilasciata da un emittente noto e attendibile. L'autorizzazione non può verificarsi quando manca l'attestazione, nel qual caso viene restituita un'attività completata. Quando è presente un'attestazione, viene calcolata l'età dell'utente. Se l'utente soddisfa l'età minima definita dal requisito, l'autorizzazione viene considerata corretta. Quando l'autorizzazione ha esito positivo, context.Succeed viene richiamata con il requisito soddisfatto come unico parametro.

Usare un gestore per più requisiti

L'esempio seguente mostra una relazione uno-a-molti in cui un gestore di autorizzazioni può gestire tre diversi tipi di requisiti:

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission ||
                     requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}

Il codice precedente attraversa una proprietà contenente i requisiti non contrassegnati PendingRequirementscome riusciti. Per un ReadPermission requisito, l'utente deve essere un proprietario o uno sponsor per accedere alla risorsa richiesta. Per un requisito EditPermission o DeletePermission , l'utente deve essere un proprietario per accedere alla risorsa richiesta.

Registrazione del gestore

I gestori vengono registrati nella raccolta di servizi durante la configurazione. Ad esempio:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

    services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

Il codice precedente viene MinimumAgeHandler registrato come singleton richiamando services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();. I gestori possono essere registrati usando una qualsiasi durata del servizio predefinita.

È possibile aggregare sia un requisito che un gestore in una singola classe implementando sia IAuthorizationRequirement che IAuthorizationHandler. Questo bundledling crea un accoppiamento stretto tra il gestore e il requisito ed è consigliato solo per requisiti e gestori semplici. La creazione di una classe che implementa entrambe le interfacce rimuove la necessità di registrare il gestore in inserimento di dipendenze a causa del PassThroughAuthorizationHandler predefinito che consente ai requisiti di gestire se stessi.

Per un buon esempio, vedere la classe AssertionRequirement in cui AssertionRequirement è un requisito e il gestore in una classe completamente autonoma.

Cosa deve restituire un gestore?

Si noti che il Handle metodo nell'esempio del gestore non restituisce alcun valore. Come è indicato uno stato di esito positivo o negativo?

  • Un gestore indica l'esito positivo chiamando context.Succeed(IAuthorizationRequirement requirement), passando il requisito che è stato convalidato correttamente.

  • Un gestore non deve gestire gli errori in genere, perché altri gestori per lo stesso requisito possono avere esito positivo.

  • Per garantire un errore, anche se altri gestori dei requisiti hanno esito positivo, chiamare context.Fail.

Se un gestore chiama context.Succeed o context.Fail, tutti gli altri gestori vengono comunque chiamati. Ciò consente ai requisiti di produrre effetti collaterali, ad esempio la registrazione, che avviene anche se un altro gestore ha convalidato o non ha superato un requisito. Se impostata su false, la InvokeHandlersAfterFailure proprietà cortocircuita l'esecuzione dei gestori quando context.Fail viene chiamata. InvokeHandlersAfterFailure il valore predefinito è true, nel qual caso vengono chiamati tutti i gestori.

Nota

I gestori di autorizzazione vengono chiamati anche se l'autenticazione non riesce.

Perché è consigliabile usare più gestori per un requisito?

Nei casi in cui si vuole che la valutazione sia su base OR , implementare più gestori per un singolo requisito. Ad esempio, Microsoft ha porte aperte solo con le schede chiave. Se lasci la carta chiave a casa, il receptionist stampa un adesivo temporaneo e apre la porta per te. In questo scenario si avrà un singolo requisito, BuildingEntry, ma più gestori, ognuno dei quali esamina un singolo requisito.

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}

BadgeEntryHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "BadgeId" &&
                                       c.Issuer == "http://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "TemporaryBadgeId" &&
                                       c.Issuer == "https://microsoftsecurity"))
        {
            // We'd also check the expiration date on the sticker.
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

Assicurarsi che entrambi i gestori siano registrati. Se uno dei due gestori ha esito positivo quando un criterio valuta BuildingEntryRequirement, la valutazione dei criteri ha esito positivo.

Usare un func per soddisfare un criterio

Possono verificarsi situazioni in cui l'adempimento di un criterio è semplice da esprimere nel codice. È possibile fornire un oggetto Func<AuthorizationHandlerContext, bool> quando si configurano i criteri con il RequireAssertion generatore di criteri.

Ad esempio, il precedente BadgeEntryHandler può essere riscritto come segue:

services.AddAuthorization(options =>
{
     options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));
});

Accedere al contesto della richiesta MVC nei gestori

Il HandleRequirementAsync metodo implementato in un gestore di autorizzazioni ha due parametri: un AuthorizationHandlerContext e l'oggetto TRequirement che si gestisce. Framework come MVC o SignalR sono liberi di aggiungere qualsiasi oggetto alla Resource proprietà su AuthorizationHandlerContext per passare informazioni aggiuntive.

Quando si usa il routing degli endpoint, l'autorizzazione viene in genere gestita dal middleware di autorizzazione. In questo caso, la proprietà è un'istanza Resource di HttpContext. Il contesto può essere usato per accedere all'endpoint corrente, che può essere usato per eseguire il probe della risorsa sottostante a cui si sta eseguendo il routing. Ad esempio:

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

Con il routing tradizionale o quando l'autorizzazione viene eseguita come parte del filtro di autorizzazione di MVC, il valore di Resource è un'istanza AuthorizationFilterContext di . Questa proprietà fornisce l'accesso a HttpContext, RouteDatae tutto il resto fornito da MVC e Razor Pages.

L'uso della proprietà è specifico del Resource framework. L'uso delle informazioni nella proprietà limita i Resource criteri di autorizzazione a framework specifici. Eseguire il cast della proprietà usando la Resourceis parola chiave e quindi verificare che il cast abbia avuto esito positivo per assicurarsi che il codice non si arresti in modo anomalo quando InvalidCastException viene eseguito in altri framework:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

A livello globale, tutti gli utenti devono essere autenticati

Per informazioni su come richiedere a livello globale l'autenticazione di tutti gli utenti, vedere Richiedere utenti autenticati.