Autorização baseada em recursos no ASP.NET Core

Este artigo descreve como autorizar utilizadores a aceder a recursos da aplicação.

Numa aplicação, um recurso é tipicamente representado por uma classe C# que inclui dados armazenados numa coleção, como um byte[] array. A classe normalmente contém metadados adicionais relacionados com o recurso, como um identificador único de recurso, datas, autores, informação de origem e um nome amigável para exibição numa interface. A coleção que contém dados de recursos é geralmente carregada a partir de conteúdos físicos de ficheiros, de um objeto de armazenamento na cloud, de um objeto em memória ou de dados de uma base de dados.

A autorização baseada em recursos requer atenção especial nas aplicações ASP.NET Core. A avaliação de atributos ocorre antes da ligação de dados e antes da execução de qualquer método que carregue um recurso. A autorização declarativa com um [Authorize] atributo não é suficiente para autorização baseada em recursos. Em vez disso, a aplicação deve invocar um método de autorização personalizado — uma abordagem conhecida como autorização imperativa.

Este artigo utiliza exemplos de componentes Razor e foca-se em cenários de autorização Blazor para ASP.NET Core 3.1 ou posteriores. Para as Páginas Razor e as orientações MVC, que se aplicam a todas as versões de ASP.NET Core, consulte os seguintes recursos:

Exemplos neste artigo utilizam construtores primários, disponíveis em C# 12 (.NET 8) ou posteriores. Para mais informações, consulte Declarar construtores primários para classes e estruturas (tutorial de documentação C#) e Construtores primários (Guia C#).

Aplicativo de exemplo

O Blazor Web App exemplo deste artigo é a BlazorWebAppAuthorization aplicação de exemplo (dotnet/AspNetCore.Docs.Samples repositório GitHub) (como descarregar). A aplicação de exemplo utiliza contas seeded com objetos de documento pré-configurados para demonstrar os exemplos deste artigo. Para mais informações, consulte o ficheiro README do exemplo (README.md).

Atenção

Esta aplicação de exemplo utiliza uma base de dados em memória para armazenar informações do utilizador, o que não é adequado para cenários de produção. A aplicação de exemplo destina-se apenas a fins de demonstração e não deve ser usada como ponto de partida para aplicações de produção.

Use uma autorização imperativa

A autorização é implementada como um IAuthorizationService, que é registado na coleção de serviços no arranque da aplicação pelo framework ASP.NET Core. O serviço é disponibilizado a componentes Razor e outras classes através de injeção de dependências:

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

IAuthorizationService tem duas sobrecargas do método AuthorizeAsync. Uma das sobrecargas aceita um recurso e um nome de política:

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

A outra sobrecarga aceita um recurso e uma coleção de requisitos (IAuthorizationRequirement) para avaliar:

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

No exemplo seguinte, que é totalmente explicado na secção Criar um manipulador baseado em recursos , o recurso protegido é carregado num objeto personalizado Document . É invocada uma AuthorizeAsync sobrecarga para determinar se o utilizador atual pode aceder ao documento com base na política de autorização "SameAuthorPolicy". Se authorizationResult.Succeeded for true, o utilizador está autorizado para o documento porque é o autor do documento (Document.Author corresponde ao Name do utilizador):

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

        ...
    }
}

Crie um handler baseado em recursos

Criar um gestor de autorização baseado em recursos é semelhante a criar um gestor de requisitos simples. Crie uma classe de requisitos personalizada e implemente uma classe handler de requisitos. Para mais informações sobre a criação de uma classe de requisitos, consulte Autorização baseada em políticas: Requisitos.

A seguinte classe de demonstração Document é utilizada:

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

A classe handler especifica o requisito e o tipo de recurso. O exemplo seguinte demonstra um manipulador a utilizar um SameAuthorRequirement requisito e um Document recurso.

Services/DocumentAuthorizationHandler.cs:

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

namespace BlazorWebAppAuthorization.Services;

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

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }

Registre o requisito e o manipulador em Program.cs:

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

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

Registre o requisito e o manipulador em Startup.ConfigureServices:

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

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();

Para mais informações sobre a criação de políticas de autorização, consulte Autorização baseada em políticas em ASP.NET Core.

O componente seguinte AccessDocument invoca uma sobrecarga de AuthorizeAsync para determinar se o utilizador atual tem permissão para ver um documento de acordo com a política de autorização "SameAuthorPolicy". Se authorizationResult.Succeeded for igual a true, o utilizador está autorizado para o documento porque é o autor do documento (Document.Author corresponde ao Name do utilizador).

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

Na aplicação de exemplo, cada utilizador da aplicação está autorizado a aceder ao documento inicial de que é autor.

Requisitos operacionais

Para tomar decisões com base nos resultados das operações CRUD (Crear, Lê, Atualizar, Eliminar), use a OperationAuthorizationRequirement classe auxiliar. A classe helper permite-lhe escrever um único handler em vez de uma classe individual para cada tipo de operação. A seguinte Operations classe estabelece todos os quatro tipos de operações 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) };
}

O seguinte DocumentAuthorizationCrudHandler processador de autorização valida a operação com base no recurso, na identidade do utilizador (função) em alguns casos e na propriedade Name do requisito:

  • Todos os utilizadores podem ler documentos.
  • Só os utilizadores do Admin cargo podem criar e atualizar documentos.
  • Apenas os utilizadores da SuperUser função podem eliminar documentos.

Services/DocumentAuthorizationCrudHandler.cs:

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

namespace BlazorWebAppAuthorization.Services;

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

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

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

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

        return Task.CompletedTask;
    }
}

Quando os serviços estão registados na aplicação:

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

Chame a sobrecarga de AuthorizeAsync com a operação para devolver o resultado da autorização.

Para autorização para criar um documento:

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

Para autorização para ler um documento:

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

Para autorização para eliminar um documento:

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

Para autorização para atualizar um documento:

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

Na página AccessDocumentCrud da aplicação de exemplo:

  • Leela (leela@contoso.com), como Admin e SuperUser, pode realizar operações CRUD completas em recursos.
  • Harry (harry@contoso.com), como o único Admin, pode criar, ler e atualizar recursos.
  • Sarah (sarah@contoso.com), sendo apenas um SuperUser, pode eliminar e ler recursos.