Autoryzacja oparta na zasobach w usłudze ASP.NET Core

W tym artykule opisano sposób autoryzacji użytkowników w celu uzyskania dostępu do zasobów aplikacji.

W aplikacji zasób jest zwykle reprezentowany przez klasę języka C#, która zawiera dane przechowywane w kolekcji, takie jak tablicabyte[]. Klasa zwykle zawiera dodatkowe metadane dotyczące zasobu, takie jak unikatowy identyfikator zasobu, daty, autorzy, informacje o źródle i przyjazna nazwa wyświetlana w interfejsie użytkownika. Kolekcja, która przechowuje dane zasobów, jest zwykle ładowana z zawartości pliku fizycznego, obiektu magazynu w chmurze, obiektu w pamięci lub danych z bazy danych.

Autoryzacja oparta na zasobach wymaga szczególnej uwagi w aplikacjach ASP.NET Core. Ocena atrybutu występuje przed powiązaniem danych i przed wykonaniem dowolnej metody, która ładuje zasób. Autoryzacja deklaratywna z atrybutem [Authorize] nie wystarcza do autoryzacji opartej na zasobach. Zamiast tego aplikacja musi wywołać niestandardową metodę autoryzacji — podejście znane jako autoryzacja imperatywne.

W tym artykule są używane przykłady składników Razor i koncentruje się na scenariuszach autoryzacji Blazor dla ASP.NET Core 3.1 lub nowszej. Informacje o Razor Pages i wskazówkach dotyczących MVC, które dotyczą wszystkich wersji platformy ASP.NET Core, można znaleźć w następujących zasobach:

Przykłady w tym artykule używają konstruktorów podstawowych dostępnych w języku C# 12 (.NET 8) lub nowszym. Aby uzyskać więcej informacji, zobacz Deklarowanie konstruktorów podstawowych dla klas i struktur (samouczek dokumentacji języka C#) i Konstruktory podstawowe (Przewodnik języka C#).

Przykładowa aplikacja

Przykład Blazor Web App dla tego artykułu to BlazorWebAppAuthorization przykładowa aplikacja (repozytorium GitHub) (dotnet/AspNetCore.Docs.Samplesjak pobrać). Przykładowa aplikacja używa wstępnie utworzonych kont z wcześniej skonfigurowanymi obiektami dokumentów, aby zilustrować przykłady opisane w tym artykule. Aby uzyskać więcej informacji, zobacz plik README próbkowego pliku (README.md).

Caution

Ta przykładowa aplikacja używa bazy danych w pamięci do przechowywania informacji o użytkowniku, które nie są odpowiednie dla scenariuszy produkcyjnych. Przykładowa aplikacja jest przeznaczona tylko do celów demonstracyjnych i nie powinna być używana jako punkt wyjścia dla aplikacji produkcyjnych.

Używanie autoryzacji imperatywnej

Autoryzacja jest zaimplementowana jako IAuthorizationService, rejestrowana w kolekcji usług podczas uruchamiania aplikacji przez platformę ASP.NET Core. Usługa jest udostępniana Razor składnikom i innym klasom za pośrednictwem wstrzykiwania zależności:

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

IAuthorizationService ma dwa przeciążenia metody AuthorizeAsync. Jedno z przeciążeń akceptuje nazwę zasobu i zasad:

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

Drugie przeciążenie przyjmuje zasób i zbiór wymagań (IAuthorizationRequirement) do oceny:

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

W poniższym przykładzie, który jest w pełni wyjaśniony w sekcji Tworzenie procedury obsługi opartej na zasobach , zabezpieczony zasób jest ładowany do obiektu niestandardowego Document . Wywoływane jest przeciążenie AuthorizeAsync w celu określenia, czy bieżący użytkownik może uzyskać dostęp do dokumentu na podstawie zasad autoryzacji "SameAuthorPolicy". Jeśli authorizationResult.Succeeded jest true, użytkownik ma dostęp do dokumentu, ponieważ jest jego autorem (wartość Document.Author jest zgodna z Name użytkownika):

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

        ...
    }
}

Tworzenie programu obsługi opartego na zasobach

Tworzenie programu obsługi autoryzacji opartej na zasobach jest podobne do tworzenia procedury obsługi zwykłych wymagań. Utwórz niestandardową klasę wymagań i zaimplementuj klasę obsługi wymagań. Aby uzyskać więcej informacji na temat tworzenia klasy wymagań, zobacz Autoryzacja oparta na zasadach w ASP.NET Core.

Używana jest następująca klasa demonstracyjna Document :

namespace BlazorWebAppAuthorization.Models;

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

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

    public Guid ID { get; set; }

    public string? Title { get; set; }
}

Klasa obsługi określa wymaganie i typ zasobu. Poniższy przykład przedstawia program obsługi wykorzystujący wymaganie SameAuthorRequirement i zasób Document.

Policies/Handlers/DocumentAuthorizationHandler.cs:

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

namespace BlazorWebAppAuthorization.Policies.Handlers;

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

Zarejestruj wymaganie i procedurę obsługi w programie Program.cs:

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

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

Zarejestruj wymaganie i procedurę obsługi w programie Startup.ConfigureServices:

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

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

Aby uzyskać więcej informacji na temat tworzenia zasad autoryzacji, zobacz Policy oparte na autoryzacji w ASP.NET Core.

Poniższy AccessDocument składnik wywołuje AuthorizeAsync przeciążenie, aby określić, czy bieżący użytkownik może wyświetlać dokument na podstawie zasad autoryzacji "SameAuthorPolicy". Jeśli authorizationResult.Succeeded jest true, to użytkownik ma uprawnienia do dokumentu, ponieważ jest jego autorem (Document.Author odpowiada wartości Name użytkownika).

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

W przykładowej aplikacji każdy użytkownik aplikacji ma autoryzowany dostęp do wstępnie utworzonego dokumentu.

Wymagania operacyjne

Aby podejmować decyzje na podstawie wyników operacji CRUD (tworzenie, odczyt, aktualizowanie, usuwanie), użyj OperationAuthorizationRequirement klasy pomocnika. Klasa pomocnika umożliwia pisanie pojedynczej procedury obsługi zamiast pojedynczej klasy dla każdego typu operacji. Poniższa Operations klasa ustanawia wszystkie cztery typy operacji 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) };
}

Następująca DocumentAuthorizationCrudHandler procedura obsługi autoryzacji weryfikuje operację przy użyciu zasobu, tożsamości użytkownika (roli) w niektórych przypadkach i właściwości wymagania Name :

  • Wszyscy użytkownicy mogą odczytywać dokumenty.
  • Tylko użytkownicy w Admin roli mogą tworzyć i aktualizować dokumenty.
  • Tylko użytkownicy w SuperUser roli mogą usuwać dokumenty.

Policies/Handlers/DocumentAuthorizationCrudHandler.cs:

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

namespace BlazorWebAppAuthorization.Policies.Handlers;

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

Gdzie usługi są rejestrowane w aplikacji:

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

Wywołaj przeciążenie AuthorizeAsync operacji , aby zwrócić wynik autoryzacji.

Aby uzyskać autoryzację w celu utworzenia dokumentu:

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

Aby uzyskać autoryzację do odczytania dokumentu:

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

Aby uzyskać autoryzację do usunięcia dokumentu:

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

Aby uzyskać autoryzację w celu zaktualizowania dokumentu:

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

Na stronie przykładowejAccessDocumentCrud aplikacji:

  • Leela (leela@contoso.com), jako Admin i SuperUser, może wykonywać pełne operacje CRUD na zasobach.
  • Harry (harry@contoso.com), jako tylko element Admin, może tworzyć, odczytywać i aktualizować zasoby.
  • Sarah (sarah@contoso.com), będąc jedynie SuperUser, może usuwać i odczytywać zasoby.