Condividi tramite


Sicurezza: autenticazione e autorizzazione in Web Forms ASP.NET e Blazor

Suggerimento

Questo contenuto è un estratto dell'eBook, Blazor per gli sviluppatori di Web Forms ASP.NET per Azure, disponibile in .NET Docs o come PDF scaricabile gratuitamente che può essere letto offline.

Anteprima della copertina dell'eBook Blazor-for-ASP-NET-Web-Forms-Developers.

La migrazione da un'applicazione Web Forms ASP.NET a Blazor richiederà quasi certamente l'aggiornamento della modalità di esecuzione dell'autenticazione e dell'autorizzazione, presupponendo che l'applicazione abbia configurato l'autenticazione. Questo capitolo illustra come eseguire la migrazione dal modello di provider universale di Web Forms ASP.NET (per appartenenza, ruoli e profili utente) e come usare ASP.NET Core Identity dalle app Blazor. Anche se questo capitolo illustra i passaggi e le considerazioni generali, i passaggi e gli script dettagliati sono disponibili nella documentazione di riferimento.

Provider universali di ASP.NET

Da ASP.NET 2.0, la piattaforma Web Forms ASP.NET supporta un modello di provider per diverse funzionalità, inclusa l'appartenenza. Il provider di appartenenze universali, insieme al provider di ruoli facoltativo, viene comunemente distribuito con le applicazioni Web Forms di ASP.NET. Offre un modo affidabile e sicuro per gestire l'autenticazione e l'autorizzazione che continua a funzionare bene oggi. L'offerta più recente di questi provider universali è disponibile come pacchetto NuGet, Microsoft.AspNet.Providers.

I provider universali funzionano con uno schema di database SQL che include tabelle come aspnet_Applications, aspnet_Membership, aspnet_Roles e aspnet_Users. Se configurata eseguendo il comando aspnet_regsql.exe, i provider installano tabelle e stored procedure che forniscono tutte le query e i comandi necessari per lavorare con i dati sottostanti. Lo schema del database e queste stored procedure non sono compatibili con i sistemi ASP.NET Identity più recenti e ASP.NET Core Identity, pertanto è necessario eseguire la migrazione dei dati esistenti nel nuovo sistema. La figura 1 mostra uno schema di tabella di esempio configurato per i provider universali.

schema dei provider universali

Il provider universale gestisce utenti, appartenenza, ruoli e profili. Agli utenti vengono assegnati identificatori univoci globali, e informazioni di base, come ad esempio userId, userName e così via, vengono archiviati nella tabella aspnet_Users. Le informazioni di autenticazione, ad esempio password, formato password, salt password, contatori di blocco e dettagli e così via, vengono archiviate nella tabella aspnet_Membership. I ruoli sono costituiti semplicemente da nomi e identificatori univoci, assegnati agli utenti tramite la tabella aspnet_UsersInRoles di associazione, fornendo una relazione molti-a-molti.

Se il sistema esistente usa ruoli oltre all'appartenenza, sarà necessario eseguire la migrazione degli account utente, delle password associate, dei ruoli e dell'appartenenza al ruolo in ASP.NET Core Identity. È anche più probabile che sia necessario aggiornare il codice in cui si eseguono controlli dei ruoli usando istruzioni if per sfruttare invece filtri dichiarativi, attributi e/o helper tag. Verranno esaminate in modo più dettagliato le considerazioni sulla migrazione alla fine di questo capitolo.

Configurazione dell'autorizzazione in Web Forms

Per configurare l'accesso autorizzato a determinate pagine in un'applicazione Web Forms ASP.NET, in genere si specifica che alcune pagine o cartelle non sono accessibili agli utenti anonimi. Questa configurazione viene eseguita nel file web.config:

<?xml version="1.0"?>
<configuration>
    <system.web>
      <authentication mode="Forms">
        <forms defaultUrl="~/home.aspx" loginUrl="~/login.aspx"
          slidingExpiration="true" timeout="2880"></forms>
      </authentication>

      <authorization>
        <deny users="?" />
      </authorization>
    </system.web>
</configuration>

La sezione di configurazione authentication configura l'autenticazione basata su form per l'applicazione. La sezione authorization viene usata per impedire utenti anonimi per l'intera applicazione. Tuttavia, è possibile fornire regole di autorizzazione più granulari in base alla posizione e applicare controlli di autorizzazione basati sui ruoli.

<location path="login.aspx">
  <system.web>
    <authorization>
      <allow users="*" />
    </authorization>
  </system.web>
</location>

La configurazione precedente, se combinata con la prima, consente agli utenti anonimi di accedere alla pagina di accesso, ignorando la restrizione a livello di sito per gli utenti non autenticati.

<location path="/admin">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

La configurazione precedente, se combinata con le altre, limita l'accesso alla cartella /admin e a tutte le risorse all'interno di esso ai membri del ruolo "Amministratori". Questa restrizione può essere applicata anche inserendo un file web.config separato all'interno della radice della cartella /admin.

Codice di autorizzazione in Web Forms

Oltre a configurare l'accesso tramite web.config, è anche possibile configurare l'accesso e il comportamento a livello di codice nell'applicazione Web Forms. Ad esempio, è possibile limitare la possibilità di eseguire determinate operazioni o visualizzare determinati dati in base al ruolo dell'utente.

Questo codice può essere usato sia nella logica code-behind che nella pagina stessa:

<% if (HttpContext.Current.User.IsInRole("Administrators")) { %>
  <a href="/admin">Go To Admin</a>
<% } %>

Oltre a controllare l'appartenenza ai ruoli utente, è anche possibile determinare se sono autenticati (anche se spesso questa operazione è più efficace usando la configurazione basata sulla posizione descritta in precedenza). Di seguito è riportato un esempio di questo approccio.

protected void Page_Load(object sender, EventArgs e)
{
    if (!User.Identity.IsAuthenticated)
    {
        FormsAuthentication.RedirectToLoginPage();
    }
    if (!Roles.IsUserInRole(User.Identity.Name, "Administrators"))
    {
        MessageLabel.Text = "Only administrators can view this.";
        SecretPanel.Visible = false;
    }
}

Nel codice precedente, il controllo degli accessi in base al ruolo (RBAC) viene usato per stabilire se determinati elementi della pagina, ad esempio SecretPanel, sono visibili in base al ruolo dell'utente corrente.

In genere, ASP.NET applicazioni Web Forms configurano la sicurezza all'interno del file web.config e quindi aggiungono controlli aggiuntivi dove necessario nelle pagine .aspx e nei relativi file code-behind .aspx.cs. La maggior parte delle applicazioni sfrutta il provider di appartenenze universali, spesso con il provider di ruoli aggiuntivo.

Identità di ASP.NET Core

Anche se ancora sottoposto ad attività di autenticazione e autorizzazione, ASP.NET Core Identity usa un set diverso di astrazioni e presupposti rispetto ai provider universali. Ad esempio, il nuovo modello di Identity supporta l'autenticazione di terze parti, consentendo agli utenti di eseguire l'autenticazione usando un account di social media o un altro provider di autenticazione attendibile. ASP.NET Core Identity supporta l'interfaccia utente per pagine di uso comune, ad esempio l'accesso, la disconnessione e la registrazione. Sfrutta EF Core per l'accesso ai dati e usa le migrazioni di EF Core per generare lo schema necessario per supportare il modello di dati. Questa introduzione a Identity su ASP.NET Core offre una buona panoramica di ciò che è incluso in ASP.NET Core Identity e come iniziare a usarlo. Se non è già stata configurata ASP.NET Core Identity nell'applicazione e nel relativo database, sarà utile iniziare.

Ruoli, attestazioni e criteri

Sia i provider universali che ASP.NET Core Identity supportano il concetto di ruoli. È possibile creare ruoli per gli utenti e assegnare utenti ai ruoli. Gli utenti possono appartenere a un numero qualsiasi di ruoli ed è possibile verificare l'appartenenza al ruolo come parte dell'implementazione dell'autorizzazione.

Oltre ai ruoli, ASP.NET Core Identity supporta i concetti di attestazioni e criteri. Anche se un ruolo deve corrispondere specificamente a un set di risorse a cui un utente in tale ruolo deve essere in grado di accedere, un'attestazione fa semplicemente parte dell'identità di un utente. Un'attestazione è una coppia di valori nome che rappresenta ciò che l'oggetto è, non le operazioni che l'oggetto può eseguire.

È possibile esaminare direttamente le attestazioni di un utente e determinare in base a questi valori se un utente deve avere accesso a una risorsa. Tuttavia, tali controlli sono spesso ripetitivi e sparsi in tutto il sistema. Un approccio migliore consiste nel definire un criterio.

Un criterio di autorizzazione è costituito da uno o più requisiti. I criteri vengono registrati come parte della configurazione del servizio di autorizzazione nel metodo ConfigureServices di Startup.cs. Ad esempio, il frammento di codice seguente configura un criterio denominato "CanadiansOnly", che ha il requisito che l'utente abbia l'attestazione Country (Paese) con il valore "Canada".

services.AddAuthorization(options =>
{
    options.AddPolicy("CanadiansOnly", policy => policy.RequireClaim(ClaimTypes.Country, "Canada"));
});

Per altre informazioni su come creare criteri personalizzati, vedere la documentazione.

Indipendentemente dal fatto che si usino criteri o ruoli, è possibile specificare che una determinata pagina nell'applicazione Blazor richiede tale ruolo o criterio con l'attributo [Authorize], applicato alla direttiva @attribute.

Richiesta di un ruolo:

@attribute [Authorize(Roles ="administrators")]

Richiesta di un criterio soddisfatto:

@attribute [Authorize(Policy ="CanadiansOnly")]

Se è necessario accedere allo stato di autenticazione, ai ruoli o alle attestazioni nel codice di un utente, esistono due modi principali per ottenere questa funzionalità. Il primo consiste nel ricevere lo stato di autenticazione come parametro a catena. Il secondo consiste nell'accedere allo stato usando un AuthenticationStateProvider inserito. I dettagli di ognuno di questi approcci sono descritti nella Blazor documentazione sulla sicurezza.

Il codice seguente illustra come ricevere AuthenticationState come parametro a catena:

[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

Con questo parametro sul posto, è possibile ottenere l'utente usando questo codice:

var authState = await authenticationStateTask;
var user = authState.User;

Il codice seguente illustra come inserire AuthenticationStateProvider:

@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider

Con il provider sul posto, è possibile ottenere l'accesso all'utente con il codice seguente:

AuthenticationState authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
ClaimsPrincipal user = authState.User;

if (user.Identity.IsAuthenticated)
{
  // work with user.Claims and/or user.Roles
}

Nota: il componente AuthorizeView, descritto più avanti in questo capitolo, fornisce un modo dichiarativo per controllare ciò che un utente vede in una pagina o un componente.

Per usare utenti e attestazioni (nelle applicazioni Server Blazor) potrebbe essere necessario inserire anche UserManager<T> (usare IdentityUser per impostazione predefinita) che è possibile usare per enumerare e modificare le attestazioni per un utente. Inserire prima il tipo e assegnarlo a una proprietà:

@inject UserManager<IdentityUser> MyUserManager

Usarlo quindi per lavorare con le attestazioni dell'utente. L'esempio seguente illustra come aggiungere e rendere persistente un'attestazione in un utente:

private async Task AddCountryClaim()
{
    var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
    var user = authState.User;
    var identityUser = await MyUserManager.FindByNameAsync(user.Identity.Name);

    if (!user.HasClaim(c => c.Type == ClaimTypes.Country))
    {
        // stores the claim in the cookie
        ClaimsIdentity id = new ClaimsIdentity();
        id.AddClaim(new Claim(ClaimTypes.Country, "Canada"));
        user.AddIdentity(id);

        // save the claim in the database
        await MyUserManager.AddClaimAsync(identityUser, new Claim(ClaimTypes.Country, "Canada"));
    }
}

Se è necessario lavorare con i ruoli, seguire lo stesso approccio. Potrebbe essere necessario inserire un oggetto RoleManager<T> (usare IdentityRole per il tipo predefinito) per elencare e gestire i ruoli stessi.

Nota: Nei progetti WebAssembly Blazor è necessario fornire API server per eseguire queste operazioni (anziché usare direttamente UserManager<T> o RoleManager<T>). Un'applicazione client WebAssembly Blazor gestisce le attestazioni e/o i ruoli chiamando in modo sicuro gli endpoint API esposti a questo scopo.

Guida alla migrazione

La migrazione da Web Forms ASP.NET e provider universali ad ASP.NET Core Identity richiede diversi passaggi:

  1. Creare lo schema del database di ASP.NET Core Identity nel database di destinazione
  2. Eseguire la migrazione dei dati dallo schema del provider universale allo schema di ASP.NET Core Identity
  3. Eseguire la migrazione della configurazione da web.config al middleware e ai servizi, in genere in Program.cs (o una classe Startup)
  4. Aggiornare singole pagine usando controlli e condizionali per usare helper tag e nuove API di identità.

Ogni passaggio viene descritto dettagliatamente nelle sezioni seguenti.

Creazione dello schema di ASP.NET Core Identity

Esistono diversi modi per creare la struttura di tabella necessaria usata per ASP.NET Core Identity. Il più semplice consiste nel creare una nuova applicazione Web ASP.NET Core. Scegliere Applicazione Web e quindi modificare il Tipo di autenticazione per usare singoli account.

nuovo progetto con singoli account

Dalla riga di comando è possibile eseguire la stessa operazione eseguendo dotnet new webapp -au Individual. Dopo aver creato l'app, eseguirla e registrarsi nel sito. È consigliabile attivare una pagina simile a quella illustrata di seguito:

pagina applica migrazioni

Fare clic sul pulsante "Applica migrazioni" e creare automaticamente le tabelle di database necessarie. Inoltre, i file di migrazione dovrebbero essere visualizzati nel progetto, come illustrato di seguito:

file di migrazione

È possibile eseguire la migrazione manualmente, senza eseguire l'applicazione Web, usando questo strumento da riga di comando:

dotnet ef database update

Se si preferisce eseguire uno script per applicare il nuovo schema a un database esistente, è possibile creare uno script per queste migrazioni dalla riga di comando. Eseguire questo comando per generare lo script:

dotnet ef migrations script -o auth.sql

Il comando precedente genererà uno script SQL nel file di output auth.sql, che può quindi essere eseguito su qualsiasi database desiderato. Se si verificano problemi durante l'esecuzione di comandi dotnet ef, assicurarsi di avere installato gli strumenti di EF Core nel sistema.

Nel caso in cui siano presenti colonne aggiuntive nelle tabelle di origine, sarà necessario identificare la posizione migliore per queste colonne nel nuovo schema. In genere, le colonne presenti nella tabella aspnet_Membership devono essere mappate alla tabella AspNetUsers. Le colonne in aspnet_Roles devono essere mappate a AspNetRoles. Eventuali colonne aggiuntive nella tabella aspnet_UsersInRoles verranno aggiunte alla tabella AspNetUserRoles.

Vale anche la pena prendere in considerazione l'inserimento di eventuali colonne aggiuntive in tabelle separate. Pertanto, le migrazioni future non dovranno tenere conto di tali personalizzazioni dello schema di identità predefinito.

Migrazione di dati da provider universali ad ASP.NET Core Identity

Dopo aver creato lo schema della tabella di destinazione, il passaggio successivo consiste nell’eseguire la migrazione dei record utente e ruolo al nuovo schema. Un elenco completo delle differenze dello schema, incluse le colonne mappate alle nuove colonne, è disponibile qui.

Per eseguire la migrazione degli utenti dall'appartenenza alle nuove tabelle di identità, è necessario seguire i passaggi descritti nella documentazione. Dopo aver seguito questi passaggi e lo script fornito, gli utenti dovranno modificare la password al successivo accesso.

È possibile eseguire la migrazione delle password utente, ma il processo è molto più complesso. Richiedendo agli utenti di aggiornare le password come parte del processo di migrazione e incoraggiare l'uso di password nuove e univoche, è probabile che migliori la sicurezza complessiva dell'applicazione.

Migrazione delle impostazioni di sicurezza da web.config all'avvio dell'app

Come indicato in precedenza, l'appartenenza e i provider di ruoli ASP.NET vengono configurati nel file web.config dell'applicazione. Poiché le app ASP.NET Core non sono associate a IIS e usano un sistema separato per la configurazione, queste impostazioni devono essere configurate altrove. Nella maggior parte dei casi, ASP.NET Core Identity è configurato nel file Program.cs. Aprire il progetto Web creato in precedenza (per generare lo schema della tabella dell’identità) ed esaminare il relativo file Program.cs (o Startup.cs).

Questo codice aggiunge il supporto per EF Core e Identity:

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
    options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

Il metodo di estensione AddDefaultIdentity viene usato per configurare Identity in modo da usare il ApplicationDbContext predefinito e il tipo di IdentityUser del framework. Se si usa un oggetto IdentityUser personalizzato, assicurarsi di specificarne il tipo qui. Se questi metodi di estensione non funzionano nell'applicazione, verificare di avere le direttive appropriate using e di disporre dei riferimenti necessari al pacchetto NuGet. Ad esempio, il progetto deve avere una versione dei pacchetti Microsoft.AspNetCore.Identity.EntityFrameworkCore e Microsoft.AspNetCore.Identity.UI a cui si fa riferimento.

Inoltre, in Program.cs dovrebbe essere visualizzato il middleware necessario configurato per il sito. In particolare, UseAuthentication e UseAuthorization devono essere configurati, e trovarsi nella posizione corretta.

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

//app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

ASP.NET Identity non configura l'accesso anonimo o basato sui ruoli alle posizioni da Program.cs. Sarà necessario eseguire la migrazione di tutti i dati di configurazione dell'autorizzazione specifici della posizione ai filtri in ASP.NET Core. Prendere nota delle cartelle e delle pagine che richiedono tali aggiornamenti. Queste modifiche verranno apportate nella sezione successiva.

Aggiornamento di singole pagine per l'uso di astrazioni di ASP.NET Core Identity

Nell'applicazione Web Forms ASP.NET, se si dispone di impostazioni web.config per negare l'accesso a determinate pagine o cartelle a utenti anonimi, è necessario eseguire la migrazione di queste modifiche aggiungendo l'attributo [Authorize] a tali pagine:

@attribute [Authorize]

Se è stato negato ulteriormente l'accesso ad eccezione di quegli utenti appartenenti a un determinato ruolo, è consigliabile eseguire la migrazione di questo comportamento aggiungendo un attributo che specifica un ruolo:

@attribute [Authorize(Roles ="administrators")]

L'attributo [Authorize] funziona solo sui componenti @page raggiunti tramite il router Blazor. L'attributo non funziona con i componenti figlio, che devono invece usare AuthorizeView.

Se si dispone di logica all'interno del markup della pagina per determinare se visualizzare codice per un determinato utente, è possibile sostituirlo con il componente AuthorizeView. Il componente AuthorizeView visualizza in modo selettivo l'interfaccia utente a seconda che l'utente sia autorizzato o meno a visualizzarlo. Espone anche una variabile context che può essere usata per accedere alle informazioni utente.

<AuthorizeView>
    <Authorized>
        <h1>Hello, @context.User.Identity.Name!</h1>
        <p>You can only see this content if you are authenticated.</p>
    </Authorized>
    <NotAuthorized>
        <h1>Authentication Failure!</h1>
        <p>You are not signed in.</p>
    </NotAuthorized>
</AuthorizeView>

È possibile accedere allo stato di autenticazione all'interno della logica procedurale accedendo all'utente da un Task<AuthenticationState configurato con l'attributo [CascadingParameter]. Questa configurazione consente di accedere all'utente, e di determinare se è autenticato e se appartiene a un ruolo specifico. Se è necessario valutare un criterio in modo procedurale, è possibile inserire un'istanza di IAuthorizationService e chiamare il metodo AuthorizeAsync su di esso. Il codice di esempio seguente illustra come ottenere informazioni sull'utente e consentire a un utente autorizzato di eseguire un'attività limitata dai criteri di content-editor.

@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

<button @onclick="@DoSomething">Do something important</button>

@code {
    [CascadingParameter]
    private Task<AuthenticationState> authenticationStateTask { get; set; }

    private async Task DoSomething()
    {
        var user = (await authenticationStateTask).User;

        if (user.Identity.IsAuthenticated)
        {
            // Perform an action only available to authenticated (signed-in) users.
        }

        if (user.IsInRole("admin"))
        {
            // Perform an action only available to users in the 'admin' role.
        }

        if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
            .Succeeded)
        {
            // Perform an action only available to users satisfying the
            // 'content-editor' policy.
        }
    }
}

Il AuthenticationState deve essere impostato come valore a catena prima di poter essere associato a un parametro a catena come questo. Questa operazione viene in genere eseguita usando il componente CascadingAuthenticationState. Questa configurazione viene in genere eseguita in App.razor:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Riepilogo

Blazor usa lo stesso modello di sicurezza di ASP.NET Core, che è ASP.NET Core Identity. La migrazione da provider universali ad ASP.NET Core Identity è relativamente semplice, presupponendo che non siano state applicate troppe personalizzazioni allo schema dei dati originale. Dopo aver eseguito la migrazione dei dati, l'uso dell'autenticazione e dell'autorizzazione nelle app Blazor è ben documentato, con supporto configurabile e a livello di codice per la maggior parte dei requisiti di sicurezza.

Riferimenti