Condividi tramite


Scenari di sicurezza aggiuntivi sul lato server ASP.NET Core Blazor

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Questo articolo illustra come configurare il lato Blazor server per scenari di sicurezza aggiuntivi, tra cui come passare token a un'app Blazor .

Nota

Gli esempi di codice in questo articolo adottano tipi di riferimento nullable (NRT) e l'analisi statica dello stato null del compilatore .NET, supportati in ASP.NET Core in .NET 6 o versione successiva. Quando la destinazione è ASP.NET Core 5.0 o versioni precedenti, rimuovere la designazione di tipo Null (?) dai string?tipi , WeatherForecast[]?TodoItem[]?, e IEnumerable<GitHubBranch>? negli esempi dell'articolo.

Passare token a un'app lato Blazor server

I token disponibili all'esterno dei Razor componenti in un'app lato Blazor server possono essere passati ai componenti con l'approccio descritto in questa sezione. L'esempio di questa sezione è incentrato sul passaggio di token token di accesso, aggiornamento e anti-richiesta (XSRF) all'appBlazor, ma l'approccio è valido per altri stati del contesto HTTP.

Nota

Il passaggio del token XSRF ai Razor componenti è utile negli scenari in cui i componenti POST a Identity o altri endpoint che richiedono la convalida. Se l'app richiede solo token di accesso e aggiornamento, è possibile rimuovere il codice del token XSRF dall'esempio seguente.

Autenticare l'app come si farebbe con una normale Razor app Pages o MVC. Effettuare il provisioning e salvare i token nell'autenticazione cookie.

Nel file Program:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

In Startup.cs:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

In Startup.cs:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});

Facoltativamente, vengono aggiunti altri ambiti con options.Scope.Add("{SCOPE}");, dove il {SCOPE} segnaposto è l'ambito aggiuntivo da aggiungere.

Definire un servizio provider di token con ambito che può essere usato all'interno dell'app per risolvere i token dall'inserimento delle dipendenze.Define a scoped token provider service that can be used within the Blazor app to resolve the tokens from dependency injection (DI).

TokenProvider.cs:

public class TokenProvider
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Program Nel file aggiungere servizi per:

  • IHttpClientFactory: usato in una WeatherForecastService classe che ottiene i dati meteo da un'API server con un token di accesso.
  • TokenProvider: contiene i token di accesso e di aggiornamento.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();

In Startup.ConfigureServices aggiungere Startup.csservizi per:

  • IHttpClientFactory: usato in una WeatherForecastService classe che ottiene i dati meteo da un'API server con un token di accesso.
  • TokenProvider: contiene i token di accesso e di aggiornamento.
services.AddHttpClient();
services.AddScoped<TokenProvider>();

Definire una classe per passare lo stato iniziale dell'app con i token di accesso e di aggiornamento.

InitialApplicationState.cs:

public class InitialApplicationState
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}

Pages/_Host.cshtml Nel file creare e istanza di InitialApplicationState e passarlo come parametro all'app:

Pages/_Layout.cshtml Nel file creare e istanza di InitialApplicationState e passarlo come parametro all'app:

Pages/_Host.cshtml Nel file creare e istanza di InitialApplicationState e passarlo come parametro all'app:

@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf

...

@{
    var tokens = new InitialApplicationState
    {
        AccessToken = await HttpContext.GetTokenAsync("access_token"),
        RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
        XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
    };
}

<component ... param-InitialState="tokens" ... />

App Nel componente (App.razor) risolvere il servizio e inizializzarlo con i dati del parametro :

@inject TokenProvider TokenProvider

...

@code {
    [Parameter]
    public InitialApplicationState? InitialState { get; set; }

    protected override Task OnInitializedAsync()
    {
        TokenProvider.AccessToken = InitialState?.AccessToken;
        TokenProvider.RefreshToken = InitialState?.RefreshToken;
        TokenProvider.XsrfToken = InitialState?.XsrfToken;

        return base.OnInitializedAsync();
    }
}

Nota

Un'alternativa all'assegnazione dello stato iniziale a TokenProvider nell'esempio precedente consiste nel copiare i dati in un servizio con ambito all'interno di per l'uso OnInitializedAsync nell'app.

Aggiungere un riferimento al pacchetto all'app per il Microsoft.AspNet.WebApi.Client pacchetto NuGet.

Nota

Per indicazioni sull'aggiunta di pacchetti alle app .NET, vedere gli articoli sotto Installare e gestire pacchetti in Flusso di lavoro dell'utilizzo di pacchetti (documentazione di NuGet). Confermare le versioni corrette del pacchetto all'indirizzo NuGet.org.

Nel servizio che effettua una richiesta API sicura inserire il provider di token e recuperare il token per la richiesta API:

WeatherForecastService.cs:

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class WeatherForecastService
{
    private readonly HttpClient http;
    private readonly TokenProvider tokenProvider;

    public WeatherForecastService(IHttpClientFactory clientFactory, 
        TokenProvider tokenProvider)
    {
        http = clientFactory.CreateClient();
        this.tokenProvider = tokenProvider;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        var token = tokenProvider.AccessToken;
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://localhost:5003/WeatherForecast");
        request.Headers.Add("Authorization", $"Bearer {token}");
        var response = await http.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? 
            Array.Empty<WeatherForecast>();
    }
}

Per un token XSRF passato a un componente, inserire TokenProvider e aggiungere il token XSRF alla richiesta POST. L'esempio seguente aggiunge il token a un endpoint di disconnessione POST. Lo scenario per l'esempio seguente è che l'endpoint di disconnessione (Areas/Identity/Pages/Account/Logout.cshtml, sottoposto a scaffolding nell'app) non specifica (IgnoreAntiforgeryTokenAttribute@attribute [IgnoreAntiforgeryToken]) perché esegue un'azione oltre a una normale operazione di disconnessione che deve essere protetta. L'endpoint richiede un token XSRF valido per elaborare correttamente la richiesta.

In un componente che presenta un pulsante disconnessione per gli utenti autorizzati:

@inject TokenProvider TokenProvider

...

<AuthorizeView>
    <Authorized>
        <form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
            <button class="nav-link btn btn-link" type="submit">Logout</button>
            <input name="__RequestVerificationToken" type="hidden" 
                value="@TokenProvider.XsrfToken">
        </form>
    </Authorized>
    <NotAuthorized>
        ...
    </NotAuthorized>
</AuthorizeView>

Impostare lo schema di autenticazione

Per un'app che usa più middleware di autenticazione e pertanto ha più schemi di autenticazione, lo schema usato Blazor può essere impostato in modo esplicito nella configurazione dell'endpoint del Program file. L'esempio seguente imposta lo schema OpenID Connect (OIDC):

Per un'app che usa più di un middleware di autenticazione e pertanto ha più schemi di autenticazione, lo schema che Blazor usa può essere impostato in modo esplicito nella configurazione dell'endpoint di Startup.cs. L'esempio seguente imposta lo schema OpenID Connect (OIDC):

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapRazorComponents<App>().RequireAuthorization(
    new AuthorizeAttribute
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    })
    .AddInteractiveServerRenderMode();
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    });

Per un'app che usa più di un middleware di autenticazione e pertanto ha più schemi di autenticazione, lo schema che Blazor usa può essere impostato in modo esplicito nella configurazione dell'endpoint di Startup.Configure. L'esempio seguente imposta lo schema MICROSOFT Entra ID:

endpoints.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
    });

Usare endpoint OIDC (OpenID Connect) v2.0

Nelle versioni di ASP.NET Core precedenti alla 5.0, la libreria di autenticazione e Blazor i modelli usano endpoint OpenID Connect (OIDC) v1.0. Per usare un endpoint v2.0 con versioni di ASP.NET Core precedenti alla 5.0, configurare l'opzione OpenIdConnectOptions.Authority in OpenIdConnectOptions:

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    }

In alternativa, l'impostazione può essere eseguita nel file delle impostazioni dell'app (appsettings.json):

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/",
    ...
  }
}

Se la descrizione di un segmento per l'autorità non è appropriata per il provider OIDC dell'app, ad esempio con provider non ME-ID, impostare direttamente la Authority proprietà. Impostare la proprietà in OpenIdConnectOptions o nel file di impostazioni dell'app con la Authority chiave .

Modifiche al codice

  • L'elenco delle attestazioni nel token ID cambia per gli endpoint v2.0. La documentazione Microsoft sulle modifiche è stata ritirata, ma le indicazioni sulle attestazioni in un token ID sono disponibili nel riferimento alle attestazioni del token ID.

  • Poiché le risorse sono specificate negli URI di ambito per gli endpoint v2.0, rimuovere l'impostazione della OpenIdConnectOptions.Resource proprietà in OpenIdConnectOptions:

    services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => 
        {
            ...
            options.Resource = "...";    // REMOVE THIS LINE
            ...
        }
    

URI ID app

  • Quando si usano endpoint v2.0, le API definiscono un App ID URIoggetto , destinato a rappresentare un identificatore univoco per l'API.
  • Tutti gli ambiti includono l'URI ID app come prefisso e gli endpoint v2.0 generano token di accesso con l'URI ID app come gruppo di destinatari.
  • Quando si usano gli endpoint V2.0, l'ID client configurato nell'API server passa dall'ID applicazione API (ID client) all'URI ID app.

appsettings.json:

{
  "AzureAd": {
    ...
    "ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
    ...
  }
}

È possibile trovare l'URI ID app da usare nella descrizione della registrazione dell'app del provider OIDC.

Gestore del circuito per acquisire gli utenti per i servizi personalizzati

Usare un CircuitHandler oggetto per acquisire un utente da AuthenticationStateProvider e impostare l'utente in un servizio. Se si vuole aggiornare l'utente, registrare un callback in AuthenticationStateChanged e accodare un Task oggetto per ottenere il nuovo utente e aggiornare il servizio. Nell'esempio seguente viene illustrato l'approccio .

Nell'esempio seguente :

  • OnConnectionUpAsync viene chiamato ogni volta che il circuito si riconnette, impostando l'utente per la durata della connessione. È necessario solo il OnConnectionUpAsync metodo, a meno che non si implementino gli aggiornamenti tramite un gestore per le modifiche di autenticazione (AuthenticationChanged nell'esempio seguente).
  • OnCircuitOpenedAsync viene chiamato per collegare il gestore di autenticazione modificato, AuthenticationChanged, per aggiornare l'utente.
  • Il catch blocco dell'attività UpdateAuthentication non esegue alcuna azione sulle eccezioni perché non è possibile segnalare le eccezioni a questo punto nell'esecuzione del codice. Se viene generata un'eccezione dall'attività, l'eccezione viene segnalata altrove nell'app.

UserService.cs:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new(new ClaimsIdentity());

    public ClaimsPrincipal GetUser() => currentUser;

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService) 
        : CircuitHandler, IDisposable
{
    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
    private readonly AuthenticationStateProvider authenticationStateProvider;
    private readonly UserService userService;

    public UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService)
    {
        this.authenticationStateProvider = authenticationStateProvider;
        this.userService = userService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}

Nel file Program:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

In Startup.ConfigureServices di Startup.cs:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

services.AddScoped<UserService>();
services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());

Usare il servizio in un componente per ottenere l'utente:

@inject UserService UserService

<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>

Per impostare l'utente nel middleware per MVC, Razor Pages e in altri scenari di ASP.NET Core, chiamare SetUser UserService in middleware personalizzato dopo l'esecuzione del middleware di autenticazione o impostare l'utente con un'implementazione IClaimsTransformation . Nell'esempio seguente viene adottato l'approccio middleware.

UserServiceMiddleware.cs:

public class UserServiceMiddleware
{
    private readonly RequestDelegate next;

    public UserServiceMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext context, UserService service)
    {
        service.SetUser(context.User);
        await next(context);
    }
}

Immediatamente prima della chiamata a app.MapRazorComponents<App>() nel Program file, chiamare il middleware:

Immediatamente prima della chiamata a app.MapBlazorHub() nel Program file, chiamare il middleware:

Immediatamente prima della chiamata a app.MapBlazorHub() in Startup.Configure di Startup.cs, chiamare il middleware:

app.UseMiddleware<UserServiceMiddleware>();

Accesso AuthenticationStateProvider nel middleware delle richieste in uscita

È AuthenticationStateProvider possibile accedere a da un oggetto DelegatingHandler per HttpClient creato con IHttpClientFactory nel middleware delle richieste in uscita usando un gestore di attività del circuito.

Nota

Per indicazioni generali sulla definizione dei gestori di delega per le richieste HTTP per le istanze HttpClient create usando IHttpClientFactory nelle app ASP.NET Core, vedere le sezioni seguenti di Effettuare richieste HTTP usando IHttpClientFactory in ASP.NET Core:

Nell'esempio seguente viene AuthenticationStateProvider usato per associare un'intestazione di nome utente personalizzata per gli utenti autenticati alle richieste in uscita.

Implementare prima di tutto la classe nella sezione seguente dell'articolo inserimento delle dipendenze :First, implement the CircuitServicesAccessor class in the following section of the Blazor dependency injection (DI) article:

Accedere ai servizi lato Blazor server da un ambito di inserimento delle dipendenze diverso

CircuitServicesAccessor Usare per accedere all'oggetto AuthenticationStateProvider nell'implementazioneDelegatingHandler.

AuthenticationStateHandler.cs:

public class AuthenticationStateHandler(
    CircuitServicesAccessor circuitServicesAccessor) 
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authStateProvider = circuitServicesAccessor.Services
            .GetRequiredService<AuthenticationStateProvider>();
        var authState = await authStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Program Nel file registrare AuthenticationStateHandler e aggiungere il gestore all'oggetto IHttpClientFactory che crea HttpClient istanze:

builder.Services.AddTransient<AuthenticationStateHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<AuthenticationStateHandler>();