Richtlinienbasierte Autorisierung in ASP.NET Core

Bei der rollenbasierten Autorisierung und der anspruchsbasierten Autorisierung werden eine Anforderung, einen Anforderungshandler und eine vorkonfigurierte Richtlinie verwendet. Diese Bausteine unterstützen die Erstellung von Autorisierungsauswertungen im Code. Das Ergebnis ist eine umfangreichere, wiederverwendbare und testbare Autorisierungsstruktur.

Eine Autorisierungsrichtlinie besteht aus mindestens einer Anforderung. Registrieren Sie diese als Teil der Autorisierungsdienstkonfiguration in der Datei Program.cs der App:

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

Im vorherigen Beispiel wird die Richtlinie „AtLeast21“ erstellt. Sie enthält eine einzelne Anforderung – die an das Mindestalter –, die als Parameter an die Anforderung übergeben wird.

IAuthorizationService

Der primäre Dienst, der bestimmt, ob die Autorisierung erfolgreich ist, lautet 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);
}

Im obigen Code sind die beiden Methoden von IAuthorizationService hervorgehoben.

IAuthorizationRequirement ist ein Markerdienst ohne Methoden, der den Mechanismus zum Nachverfolgen des Erfolgs von Autorisierungen bereitstellt.

Jeder IAuthorizationHandler ist dafür verantwortlich zu überprüfen, ob die Anforderungen erfüllt werden:

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

Die AuthorizationHandlerContext-Klasse verwendet der Handler, um zu markieren, ob die Anforderungen erfüllt wurden:

 context.Succeed(requirement)

Der folgende Code zeigt die vereinfachte (und mit Kommentaren versehene) Standardimplementierung des Autorisierungsdiensts:

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

Der folgende Code zeigt eine typische Konfiguration für den Autorisierungsdienst:

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

Verwenden Sie IAuthorizationService, [Authorize(Policy = "Something")] oder RequireAuthorization("Something") für die Autorisierung.

Anwenden von Richtlinien auf MVC-Controller

Informationen zu Apps, die Razor Pages verwenden, finden Sie im Abschnitt Anwenden von Richtlinien auf Razor Pages.

Die Richtlinien werden auf die Controller angewandt, indem das [Authorize]-Attribut mit dem Richtliniennamen verwendet wird:

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

Wenn mehrere Richtlinien auf Controller- und Aktionsebene angewandt werden, müssen alle Richtlinien erfüllt werden, bevor der Zugriff gewährt wird:

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

Anwenden von Richtlinien auf Razor Pages

Sie wenden Richtlinien auf Razor Pages an, indem Sie das [Authorize]-Attribut mit dem Richtliniennamen verwenden. Beispiel:

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

namespace AuthorizationPoliciesSample.Pages;

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

Richtlinien können nicht auf Ebene der Razor Pages-Handler angewandt werden, sie müssen auf die Seite angewandt werden.

Richtlinien können auch mithilfe einer Autorisierungskonvention auf Razor Pages angewandt werden.

Anwenden von Richtlinien auf Endpunkte

Sie wenden Richtlinien auf Endpunkte an, indem Sie RequireAuthorization mit dem Richtliniennamen verwenden. Beispiel:

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

Anforderungen

Eine Autorisierungsanforderung ist eine Sammlung von Datenparametern, die eine Richtlinie zum Auswerten des aktuellen Benutzerprinzipals verwenden kann. In der Beispielrichtlinie „AtLeast21“ ist die Anforderung ein einzelner Parameter, nämlich das Mindestalter. Eine Anforderung implementiert IAuthorizationRequirement, bei der es sich um eine leere Markerschnittstelle handelt. Eine parametrisierte Anforderung an das Mindestalter kann wie folgt implementiert werden:

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

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

    public int MinimumAge { get; }
}

Wenn eine Autorisierungsrichtlinie mehrere Autorisierungsanforderungen enthält, müssen alle Anforderungen erfüllt sein, damit die Richtlinie als erfolgreich ausgewertet wird. Anders ausgedrückt: Mehrere Autorisierungsanforderungen, die einer einzelnen Autorisierungsrichtlinie hinzugefügt wurden, sind mit AND verkettet.

Hinweis

Eine Anforderung benötigt keine Daten oder Eigenschaften.

Autorisierungshandler

Autorisierungshandler sind für die Auswertung der Eigenschaften einer Anforderung verantwortlich. Der Autorisierungshandler wertet die Anforderungen anhand eines bereitgestellten AuthorizationHandlerContext aus und bestimmt, ob der Zugriff zulässig ist.

Eine Anforderung kann über mehrere Handler verfügen. Ein Handler kann AuthorizationHandler<TRequirement> erben, wobei TRequirement die zu behandelnde Anforderung ist. Alternativ kann ein Handler IAuthorizationHandler auch direkt implementieren, um mehrere Anforderungstypen zu verarbeiten.

Verwenden eines Handlers für eine Anforderung

Das folgende Beispiel zeigt eine 1:1-Beziehung, in der ein Handler für das Mindestalter nur eine Anforderung behandelt:

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

Der obige Code bestimmt, ob der Geburtsdatumsanspruch des aktuellen Benutzerprinzipals von einem bekannten und vertrauenswürdigen Aussteller stammt. Es kann keine Autorisierung erfolgen, wenn der Anspruch fehlt. In diesem Fall wird eine abgeschlossene Aufgabe zurückgegeben. Wenn ein Anspruch vorhanden ist, wird Benutzeralter berechnet. Wenn der oder die Benutzer*in das durch die Anforderung definierte Mindestalter hat, gilt die Autorisierung als erfolgreich. Wenn die Autorisierung erfolgreich ist, wird context.Succeed mit der erfüllten Anforderung als einzigem Parameter aufgerufen.

Verwenden eines Handlers für mehrere Anforderungen

Das folgende Beispiel zeigt eine 1:n-Beziehung, in der ein Berechtigungshandler drei verschiedene Anforderungstypen behandeln kann:

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

Der obige Code durchläuft PendingRequirements, eine Eigenschaft, die Anforderungen enthält, die als nicht erfolgreich markiert sind. Für eine ReadPermission-Anforderung müssen Benutzer*innen entweder Besitzer*in oder Sponsor*in sein, um auf die angeforderte Ressource zugreifen zu können. Für eine EditPermission- oder DeletePermission-Anforderung müssen sie Besitzer*in sein, um auf die angeforderte Ressource zuzugreifen.

Handlerregistrierung

Sie registrieren Handler während der Konfiguration in der Dienstsammlung. Beispiel:

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

Im obigen Code wird MinimumAgeHandler als Singleton registriert. Handler können mit einer der integrierten Dienstlebensdauern registriert werden.

Es ist möglich, eine Anforderung und einen Handler in einer einzelnen Klasse zu bündeln, die sowohl als IAuthorizationRequirement auch IAuthorizationHandler implementiert. Diese Bündelung sorgt für eine enge Kopplung zwischen Handler und Anforderung und wird nur für einfache Anforderungen und Handler empfohlen. Durch das Erstellen einer Klasse, die beide Schnittstellen implementiert, entfällt die Notwendigkeit, den Handler in DI zu registrieren, da der integrierte PassThroughAuthorizationHandler den Anforderungen ermöglicht, sich selbst zu behandeln.

In der AssertionRequirement-Klasse finden Sie ein gutes Beispiel, bei dem AssertionRequirement sowohl eine Anforderung als auch der Handler in einer vollständig eigenständigen Klasse ist.

Was sollten Handler zurückgeben?

Beachten Sie, dass die Handle-Methode im Handlerbeispiel keinen Wert zurückgibt. Wie wird ein erfolgreicher oder fehlerhafter Status angezeigt?

  • Ein Handler gibt einen Erfolg an, indem er context.Succeed(IAuthorizationRequirement requirement) aufruft und die Anforderung übergibt, die erfolgreich überprüft wurde.

  • Ein Handler muss Fehler im Allgemeinen nicht behandeln, da andere Handler für dieselbe Anforderung möglicherweise erfolgreich sind.

  • Rufen Sie context.Fail auf, um Fehler zurückzugeben, auch wenn andere Anforderungshandler erfolgreich sind.

Wenn ein Handler context.Succeed oder context.Fail aufruft, werden weiterhin alle anderen Handler aufgerufen. Dadurch können Anforderungen Nebenwirkungen erzeugen (z. B. zur Protokollierung), die selbst dann erfolgen, wenn ein anderer Handler eine Anforderung als erfolgreich oder fehlerhaft überprüft hat. Bei Festlegung auf false umgeht die InvokeHandlersAfterFailure-Eigenschaft die Ausführung von Handlern, wenn context.Fail aufgerufen wird. InvokeHandlersAfterFailure ist standardmäßig auf true festgelegt, sodass alle Handler aufgerufen werden.

Hinweis

Autorisierungshandler werden auch dann aufgerufen, wenn die Authentifizierung fehlschlägt. Auch Handler können in beliebiger Reihenfolge ausgeführt werden, Sie können sich also nicht darauf verlassen, dass sie in einer bestimmten Reihenfolge aufgerufen werden.

Wann sollte ich mehrere Handler für eine Anforderung verwenden?

In Fällen, in denen die Auswertung nach OR-Logik erfolgen soll, implementieren Sie mehrere Handler für eine einzelne Anforderung. Bei Microsoft gibt es beispielsweise Türen, die nur mit Schlüsselkarten geöffnet werden können. Wenn Sie Ihre Schlüsselkarte zu Hause vergessen, druckt eine Person an der Rezeption einen temporären Aufkleber und öffnet die Tür für Sie. In diesem Szenario verfügen Sie über eine einzelne Anforderung, BuildingEntry, aber mehrere Handler, die jeweils eine einzelne Anforderung untersuchen.

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

Stellen Sie sicher, dass beide Handler registriert sind. Wenn einer der Handler erfolgreich ist, wenn eine Richtlinie die BuildingEntryRequirement auswertet, ist die Richtlinienauswertung erfolgreich.

Verwenden einer Funktion zum Erfüllen einer Richtlinie

In bestimmten Situationen kann es sinnvoll sein, die Erfüllung einer Richtlinie einfach im Code auszudrücken. Es ist möglich, beim Konfigurieren einer Richtlinie mit dem RequireAssertion-Richtlinien-Generator einen Func<AuthorizationHandlerContext, bool> bereitzustellen.

Der obige BadgeEntryHandler könnte z. B. wie folgt umgeschrieben werden:

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

Zugreifen auf den MVC-Anforderungskontext in Handlern

Die HandleRequirementAsync-Methode verfügt über zwei Parameter: einen AuthorizationHandlerContext und die TRequirement, die behandelt wird. Frameworks wie MVC oder SignalR können der Resource-Eigenschaft auf im AuthorizationHandlerContext beliebige Objekte hinzufügen, um zusätzliche Informationen zu übergeben.

Bei Verwendung von Endpunktrouting wird die Autorisierung in der Regel von der Autorisierungsmiddleware behandelt. In diesem Fall ist die Resource-Eigenschaft eine Instanz von HttpContext. Der Kontext kann für den Zugriff auf den aktuellen Endpunkt verwendet werden, der wiederum zum Testen der zugrunde liegenden Ressource verwendet werden kann, an die Sie weiterleiten. Beispiel:

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

Beim herkömmlichen Routing oder bei einer Autorisierung im Rahmen des Autorisierungsfilters von MVC ist der Wert von Resource ein AuthorizationFilterContext-Instanz. Diese Eigenschaft bietet Zugriff auf HttpContext, RouteData und alles andere, was von MVC und Razor Pages bereitgestellt wird.

Die Verwendung der Resource-Eigenschaft ist frameworkspezifisch. Die Verwendung von Informationen in der Resource-Eigenschaft schränkt Ihre Autorisierungsrichtlinien auf bestimmte Frameworks ein. Wandeln Sie die Resource-Eigenschaft mithilfe des Schlüsselworts is um, und vergewissern Sie sich dann, dass die Umwandlung erfolgreich war, damit Ihr Code nicht mit einer InvalidCastException abstürzt, wenn er in anderen Frameworks ausgeführt wird:

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

Globale Erzwingen der Authentifizierung aller Benutzer*innen

Informationen dazu, wie global die Authentifizierung aller Benutzer angefordert werden kann, finden Sie unter Authentifizierte Benutzer erforderlich.

Beispiel für die Autorisierung mit einem externen Dienst

Der Beispielcode auf AspNetCore.Docs.Samples zeigt, wie zusätzliche Autorisierungsanforderungen mit einem externen Autorisierungsdienst implementiert werden. Das Beispielprojekt Contoso.API wird mit Azure AD geschützt. Eine zusätzliche Autorisierungsprüfung aus dem Contoso.Security.API-Projekt gibt Nutzdaten zurück, die beschreiben, ob die Contoso.API-Client-App die GetWeather-API aufrufen kann.

Das Beispiel konfigurieren

  1. Erstellen Sie eine Anwendungsregistrierung in Ihrem Azure Active Directory-Mandanten (Azure AD):
  • Weisen Sie ihr eine AppRole zu.
  • Fügen Sie unter den API-Berechtigungen die AppRole als Berechtigung hinzu, und weisen Sie eine Administratoreinwilligung zu. Beachten Sie, dass diese App-Registrierung in diesem Setup sowohl die API als auch den Client darstellt, der die API aufruft. Wenn Sie möchten, können Sie zwei App-Registrierungen erstellen. Wenn Sie dieses Setup verwenden, dürfen Sie nur die API-Berechtigungen ausführen und müssen AppRole als Berechtigungsschritt ausschließlich für den Client hinzufügen. Es muss nur für die Client-App-Registrierung ein geheimer Clientschlüssel generiert werden.
  1. Konfigurieren Sie das Contoso.API-Projekt mit den folgenden Einstellungen:
{
  "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. Konfigurieren Sie Contoso.Security.API mit den folgenden Einstellungen:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "<Use the appropriate Client Id representing the Client calling the API>"
  ]
}
  1. Importieren Sie die Datei ContosoAPI.postman_collection.json in Postman, und konfigurieren Sie eine Umgebung mit Folgendem:

    • ClientId: Client-ID aus der App-Registrierung, die den Client darstellt, der die API aufruft
    • clientSecret: geheimer Clientschlüssel aus der App-Registrierung, der den Client darstellt, der die API aufruft
    • TenantId: Mandanten-ID aus den AAD-Eigenschaften
  2. Führen Sie die Lösung aus, und rufen Sie die API mithilfe von Postman auf. Sie können im Contoso.Security.API.SecurityPolicyController Breakpoints hinzufügen und beobachten, wie die Client-ID übergeben wird, mit der bestätigt wird, ob der Client Wetterdaten abrufen darf.

Zusätzliche Ressourcen

Bei der rollenbasierten Autorisierung und der anspruchsbasierten Autorisierung werden eine Anforderung, einen Anforderungshandler und eine vorkonfigurierte Richtlinie verwendet. Diese Bausteine unterstützen die Erstellung von Autorisierungsauswertungen im Code. Das Ergebnis ist eine umfangreichere, wiederverwendbare und testbare Autorisierungsstruktur.

Eine Autorisierungsrichtlinie besteht aus mindestens einer Anforderung. Sie wird im Rahmen der Konfiguration des Autorisierungsdiensts in der Startup.ConfigureServices-Methode registriert:

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

Im vorherigen Beispiel wird die Richtlinie „AtLeast21“ erstellt. Sie enthält eine einzelne Anforderung – die an das Mindestalter –, die als Parameter an die Anforderung übergeben wird.

IAuthorizationService

Der primäre Dienst, der bestimmt, ob die Autorisierung erfolgreich ist, lautet 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);
}

Im obigen Code sind die beiden Methoden von IAuthorizationService hervorgehoben.

IAuthorizationRequirement ist ein Markerdienst ohne Methoden, der den Mechanismus zum Nachverfolgen des Erfolgs von Autorisierungen bereitstellt.

Jeder IAuthorizationHandler ist dafür verantwortlich zu überprüfen, ob die Anforderungen erfüllt werden:

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

Die AuthorizationHandlerContext-Klasse verwendet der Handler, um zu markieren, ob die Anforderungen erfüllt wurden:

 context.Succeed(requirement)

Der folgende Code zeigt die vereinfachte (und mit Kommentaren versehene) Standardimplementierung des Autorisierungsdiensts:

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

Der folgende Code zeigt typische 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();
}

Verwenden Sie IAuthorizationService oder [Authorize(Policy = "Something")] für die Autorisierung.

Anwenden von Richtlinien auf MVC-Controller

Wenn Sie Razor Pages verwenden, lesen Sie den Abschnitt Anwenden von Richtlinien auf Razor Pages in diesem Dokument.

Die Richtlinien werden auf den Controller angewendet, indem das [Authorize]-Attribut mit dem Richtliniennamen verwendet wird. Beispiel:

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

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

Anwenden von Richtlinien auf Razor Pages

Die Richtlinien werden auf Razor Pages angewandt, indem das [Authorize]-Attribut mit dem Richtliniennamen verwendet wird. Beispiel:

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

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

Richtlinien können nicht auf Ebene der Razor Pages-Handler angewandt werden, sie müssen auf die Seite angewandt werden.

Richtlinien können mithilfe einer Autorisierungskonvention auf Razor Pages angewandt werden.

Anforderungen

Eine Autorisierungsanforderung ist eine Sammlung von Datenparametern, die eine Richtlinie zum Auswerten des aktuellen Benutzerprinzipals verwenden kann. In der Beispielrichtlinie „AtLeast21“ ist die Anforderung ein einzelner Parameter, nämlich das Mindestalter. Eine Anforderung implementiert IAuthorizationRequirement, bei der es sich um eine leere Markerschnittstelle handelt. Eine parametrisierte Anforderung an das Mindestalter kann wie folgt implementiert werden:

using Microsoft.AspNetCore.Authorization;

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

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

Wenn eine Autorisierungsrichtlinie mehrere Autorisierungsanforderungen enthält, müssen alle Anforderungen erfüllt sein, damit die Richtlinie als erfolgreich ausgewertet wird. Anders ausgedrückt: Mehrere Autorisierungsanforderungen, die einer einzelnen Autorisierungsrichtlinie hinzugefügt wurden, sind mit AND verkettet.

Hinweis

Eine Anforderung benötigt keine Daten oder Eigenschaften.

Autorisierungshandler

Autorisierungshandler sind für die Auswertung der Eigenschaften einer Anforderung verantwortlich. Der Autorisierungshandler wertet die Anforderungen anhand eines bereitgestellten AuthorizationHandlerContext aus und bestimmt, ob der Zugriff zulässig ist.

Eine Anforderung kann über mehrere Handler verfügen. Ein Handler kann AuthorizationHandler<TRequirement> erben, wobei TRequirement die zu behandelnde Anforderung ist. Alternativ kann ein Handler auch IAuthorizationHandler implementieren, um mehrere Anforderungstypen zu verarbeiten.

Verwenden eines Handlers für eine Anforderung

Das folgende Beispiel zeigt eine 1:1-Beziehung, in der ein Handler für das Mindestalter nur eine Anforderung behandelt:

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

Der obige Code bestimmt, ob der Geburtsdatumsanspruch des aktuellen Benutzerprinzipals von einem bekannten und vertrauenswürdigen Aussteller stammt. Es kann keine Autorisierung erfolgen, wenn der Anspruch fehlt. In diesem Fall wird eine abgeschlossene Aufgabe zurückgegeben. Wenn ein Anspruch vorhanden ist, wird Benutzeralter berechnet. Wenn der oder die Benutzer*in das durch die Anforderung definierte Mindestalter hat, gilt die Autorisierung als erfolgreich. Wenn die Autorisierung erfolgreich ist, wird context.Succeed mit der erfüllten Anforderung als einzigem Parameter aufgerufen.

Verwenden eines Handlers für mehrere Anforderungen

Das folgende Beispiel zeigt eine 1:n-Beziehung, in der ein Berechtigungshandler drei verschiedene Anforderungstypen behandeln kann:

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

Der obige Code durchläuft PendingRequirements, eine Eigenschaft, die Anforderungen enthält, die als nicht erfolgreich markiert sind. Für eine ReadPermission-Anforderung müssen Benutzer*innen entweder Besitzer*in oder Sponsor*in sein, um auf die angeforderte Ressource zugreifen zu können. Für eine EditPermission- oder DeletePermission-Anforderung müssen die Benutzer*innen Besitzer*in sein, um auf die angeforderte Ressource zuzugreifen.

Handlerregistrierung

Handler werden während der Konfiguration in der Dienstsammlung registriert. Beispiel:

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

Im obigen Code wird MinimumAgeHandler durch einen Aufruf von services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>(); als Singleton registriert. Handler können mit einer der integrierten Dienstlebensdauern registriert werden.

Es ist möglich, eine Anforderung und einen Handler in einer einzelnen Klasse zu bündeln, die sowohl als IAuthorizationRequirement auch IAuthorizationHandler implementiert. Diese Bündelung sorgt für eine enge Kopplung zwischen Handler und Anforderung und wird nur für einfache Anforderungen und Handler empfohlen. Durch das Erstellen einer Klasse, die beide Schnittstellen implementiert, entfällt die Notwendigkeit, den Handler in DI zu registrieren, da der integrierte PassThroughAuthorizationHandler den Anforderungen ermöglicht, sich selbst zu behandeln.

In der AssertionRequirement-Klasse finden Sie ein gutes Beispiel, bei dem AssertionRequirement sowohl eine Anforderung als auch der Handler in einer vollständig eigenständigen Klasse ist.

Was sollten Handler zurückgeben?

Beachten Sie, dass die Handle-Methode im Handlerbeispiel keinen Wert zurückgibt. Wie wird ein erfolgreicher oder fehlerhafter Status angezeigt?

  • Ein Handler gibt einen Erfolg an, indem er context.Succeed(IAuthorizationRequirement requirement) aufruft und die Anforderung übergibt, die erfolgreich überprüft wurde.

  • Ein Handler muss Fehler im Allgemeinen nicht behandeln, da andere Handler für dieselbe Anforderung möglicherweise erfolgreich sind.

  • Rufen Sie context.Fail auf, um Fehler zurückzugeben, auch wenn andere Anforderungshandler erfolgreich sind.

Wenn ein Handler context.Succeed oder context.Fail aufruft, werden weiterhin alle anderen Handler aufgerufen. Dadurch können Anforderungen Nebenwirkungen erzeugen (z. B. zur Protokollierung), die selbst dann erfolgen, wenn ein anderer Handler eine Anforderung als erfolgreich oder fehlerhaft überprüft hat. Bei Festlegung auf false umgeht die InvokeHandlersAfterFailure-Eigenschaft die Ausführung von Handlern, wenn context.Fail aufgerufen wird. InvokeHandlersAfterFailure ist standardmäßig auf true festgelegt, sodass alle Handler aufgerufen werden.

Hinweis

Autorisierungshandler werden auch dann aufgerufen, wenn die Authentifizierung fehlschlägt.

Wann sollte ich mehrere Handler für eine Anforderung verwenden?

In Fällen, in denen die Auswertung nach OR-Logik erfolgen soll, implementieren Sie mehrere Handler für eine einzelne Anforderung. Bei Microsoft gibt es beispielsweise Türen, die nur mit Schlüsselkarten geöffnet werden können. Wenn Sie Ihre Schlüsselkarte zu Hause vergessen, druckt eine Person an der Rezeption einen temporären Aufkleber und öffnet die Tür für Sie. In diesem Szenario verfügen Sie über eine einzelne Anforderung, BuildingEntry, aber mehrere Handler, die jeweils eine einzelne Anforderung untersuchen.

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

Stellen Sie sicher, dass beide Handler registriert sind. Wenn einer der Handler erfolgreich ist, wenn eine Richtlinie die BuildingEntryRequirement auswertet, ist die Richtlinienauswertung erfolgreich.

Verwenden einer Funktion zum Erfüllen einer Richtlinie

In bestimmten Situationen kann es sinnvoll sein, die Erfüllung einer Richtlinie einfach im Code auszudrücken. Es ist möglich, beim Konfigurieren einer Richtlinie mit dem RequireAssertion-Richtlinien-Generator einen Func<AuthorizationHandlerContext, bool> bereitzustellen.

Der obige BadgeEntryHandler könnte z. B. wie folgt umgeschrieben werden:

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

Zugreifen auf den MVC-Anforderungskontext in Handlern

Die HandleRequirementAsync-Methode, die Sie in einem Autorisierungshandler implementieren, weist zwei Parameter auf: einen AuthorizationHandlerContext und die TRequirement, die Sie behandeln. Frameworks wie MVC oder SignalR können der Resource-Eigenschaft auf im AuthorizationHandlerContext beliebige Objekte hinzufügen, um zusätzliche Informationen zu übergeben.

Bei Verwendung von Endpunktrouting wird die Autorisierung in der Regel von der Autorisierungsmiddleware behandelt. In diesem Fall ist die Resource-Eigenschaft eine Instanz von HttpContext. Der Kontext kann für den Zugriff auf den aktuellen Endpunkt verwendet werden, der wiederum zum Testen der zugrunde liegenden Ressource verwendet werden kann, an die Sie weiterleiten. Beispiel:

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

Beim herkömmlichen Routing oder bei einer Autorisierung im Rahmen des Autorisierungsfilters von MVC ist der Wert von Resource ein AuthorizationFilterContext-Instanz. Diese Eigenschaft bietet Zugriff auf HttpContext, RouteData und alles andere, was von MVC und Razor Pages bereitgestellt wird.

Die Verwendung der Resource-Eigenschaft ist frameworkspezifisch. Die Verwendung von Informationen in der Resource-Eigenschaft schränkt Ihre Autorisierungsrichtlinien auf bestimmte Frameworks ein. Wandeln Sie die Resource-Eigenschaft mithilfe des Schlüsselworts is um, und vergewissern Sie sich dann, dass die Umwandlung erfolgreich war, damit Ihr Code nicht mit einer InvalidCastException abstürzt, wenn er in anderen Frameworks ausgeführt wird:

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

Globale Erzwingen der Authentifizierung aller Benutzer*innen

Informationen dazu, wie global die Authentifizierung aller Benutzer angefordert werden kann, finden Sie unter Authentifizierte Benutzer erforderlich.