Condividi tramite


archiviazione del browser protetta ASP.NET Core Blazor

Per i dati temporanei che l'utente sta creando attivamente, una destinazione di archiviazione comunemente utilizzata sono le collezioni del browser localStorage e sessionStorage.

  • localStorage è vincolato all'istanza del browser. Se l'utente ricarica la pagina o si chiude e riapre il browser, lo stato persiste. Se l'utente apre più schede del browser, lo stato viene condiviso tra le schede. I dati persistono in localStorage fino a quando non vengono cancellati in modo esplicito. I dati localStorage per un documento caricato in una sessione "esplorazione privata" o "in incognito" vengono cancellati quando viene chiusa l'ultima scheda "privata".
  • sessionStorage è limitato alla scheda del browser. Se l'utente ricarica la scheda, lo stato persiste. Se l'utente chiude la scheda o il browser, lo stato viene perso. Se l'utente apre più schede del browser, ogni scheda ha una propria versione indipendente dei dati.

In genere, sessionStorage è più sicuro da usare. sessionStorage evita il rischio che un utente apra più schede e riscontri quanto segue:

  • Bug nell'archiviazione dello stato tra le schede.
  • Comportamento confuso quando una scheda sovrascrive lo stato di altre schede.

localStorage è la scelta migliore se l'app deve mantenere lo stato durante la chiusura e la riapertura del browser.

Avvertenze per l'uso dell'archiviazione del browser:

  • Analogamente all'uso di un database lato server, il caricamento e il salvataggio dei dati sono asincroni.
  • La pagina richiesta non esiste nel browser durante il pre-rendering, quindi l'archiviazione locale non è disponibile durante il pre-rendering.
  • L'archiviazione di alcuni kilobyte di dati è ragionevole per rendere persistenti le app lato Blazor server. Oltre alcuni kilobyte, è necessario considerare le implicazioni sulle prestazioni perché i dati vengono caricati e salvati in rete.
  • Gli utenti possono visualizzare o manomettere i dati. ASP.NET Core Data Protection può ridurre il rischio. Ad esempio, ASP.NET Core Protected Browser Storage usa ASP.NET Protezione dati di base.

I pacchetti NuGet di terze parti forniscono API per l'uso di localStorage e sessionStorage. È opportuno prendere in considerazione la scelta di un pacchetto che utilizza ASP.NET Core Data Protection in modo trasparente. La protezione dei dati crittografa i dati archiviati e riduce il rischio potenziale di manomissione dei dati archiviati. Se i dati serializzati json vengono archiviati in testo normale, gli utenti possono visualizzare i dati usando gli strumenti di sviluppo del browser e modificare anche i dati archiviati. La protezione dei dati semplici non è un problema. Ad esempio, la lettura o la modifica del colore archiviato di un elemento dell'interfaccia utente non rappresenta un rischio significativo per la sicurezza per l'utente o l'organizzazione. Evitare di consentire agli utenti di controllare o manomettere i dati sensibili.

ASP.NET Core Archiviazione Protetta del Browser

ASP.NET Core Protected Browser Storage sfrutta ASP.NET protezione dei dati di base per localStorage e sessionStorage.

Annotazioni

L'archiviazione browser protetta si basa su ASP.NET Core Data Protection ed è supportata solo per le app Blazor sul server.

Avvertimento

Microsoft.AspNetCore.ProtectedBrowserStorage è un pacchetto sperimentale non supportato che non è destinato all'uso in produzione.

Il pacchetto è disponibile solo per l'uso nelle app ASP.NET Core 3.1.

Configurazione

  1. Aggiungere un riferimento al pacchetto a Microsoft.AspNetCore.ProtectedBrowserStorage.

    Annotazioni

    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). Verificare le versioni corrette dei pacchetti in NuGet.org.

  2. _Host.cshtml Nel file aggiungere lo script seguente all'interno del tag di chiusura</body>:

    <script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>
    
  3. In Startup.ConfigureServices, chiamare AddProtectedBrowserStorage per aggiungere i servizi localStorage e sessionStorage alla raccolta di servizi.

    services.AddProtectedBrowserStorage();
    

Salvare e caricare i dati all'interno di un componente

In qualsiasi componente che richiede il caricamento o il salvataggio dei dati nell'archiviazione del browser, usare la @inject direttiva per inserire un'istanza di uno dei seguenti elementi:

  • ProtectedLocalStorage
  • ProtectedSessionStorage

La scelta dipende dal percorso di archiviazione del browser che si vuole usare. Nell'esempio sessionStorage seguente viene usato:

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

La @using direttiva può essere inserita nel file dell'app _Imports.razor anziché nel componente. L'uso del _Imports.razor file rende lo spazio dei nomi disponibile per segmenti più grandi dell'app o dell'intera app.

Per rendere persistente il valore del componente di un'app basata sul progetto modello , modificare il metodo per utilizzare :

private async Task IncrementCount()
{
    currentCount++;
    await ProtectedSessionStore.SetAsync("count", currentCount);
}

In app più grandi e più realistiche, l'archiviazione dei singoli campi è uno scenario improbabile. È più probabile che le app archiviino interi oggetti modello che includono uno stato complesso. ProtectedSessionStore serializza e deserializza automaticamente i dati JSON per archiviare oggetti di stato complessi.

Nell'esempio di codice precedente i currentCount dati vengono archiviati come sessionStorage['count'] nel browser dell'utente. I dati non vengono archiviati in testo normale, ma sono protetti usando ASP.NET Protezione dati di base. I dati crittografati possono essere controllati se sessionStorage['count'] vengono valutati nella console di sviluppo del browser.

Per recuperare i currentCount dati se l'utente torna al Counter componente in un secondo momento, incluso se l'utente si trova in un nuovo circuito, usare ProtectedSessionStore.GetAsync:

protected override async Task OnInitializedAsync()
{
    var result = await ProtectedSessionStore.GetAsync<int>("count");
    currentCount = result.Success ? result.Value : 0;
}
protected override async Task OnInitializedAsync()
{
    currentCount = await ProtectedSessionStore.GetAsync<int>("count");
}

Se i parametri del componente includono lo stato di navigazione, chiamare ProtectedSessionStore.GetAsync e assegnare un risultato non-null in OnParametersSetAsync, non in OnInitializedAsync. OnInitializedAsync viene chiamato una sola volta quando viene creata la prima istanza del componente. OnInitializedAsync non viene chiamato di nuovo in un secondo momento se l'utente passa a un URL diverso mentre rimane nella stessa pagina. Per altre informazioni, vedere Ciclo di vita dei componenti di ASP.NET Core Razor.

Avvertimento

Gli esempi in questa sezione funzionano solo se il server non dispone di prerendering abilitato. Con il prerendering abilitato, viene generato un errore che spiega che non è possibile eseguire chiamate di interoperabilità JavaScript perché il componente viene prerenderizzato.

Disabilitare la prerendering o aggiungere codice aggiuntivo per lavorare con la prerendering. Per ulteriori informazioni sulla scrittura di codice che funziona con il prerendering, vedere la sezione Gestire il prerendering.

Gestire lo stato di caricamento

Poiché l'archiviazione del browser è accessibile in modo asincrono tramite una connessione di rete, è sempre presente un periodo di tempo prima che i dati vengano caricati e disponibili per un componente. Per ottenere risultati ottimali, eseguire il rendering di un messaggio durante il caricamento è in corso anziché visualizzare dati vuoti o predefiniti.

Un approccio consiste nel tenere traccia se i dati sono null, il che significa che i dati sono ancora in fase di caricamento. Nel componente predefinito Counter il conteggio viene mantenuto in un oggetto int. Rendere currentCount nullable aggiungendo un punto interrogativo (?) al tipo (int):

private int? currentCount;

Anziché visualizzare sempre il conteggio e il pulsante Increment, visualizzare questi elementi solo se i dati vengono caricati, controllando HasValue.

@if (currentCount.HasValue)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

Gestire il pre-rendering

Durante il prerendering:

  • Non esiste una connessione interattiva al browser dell'utente.
  • Il browser non ha ancora una pagina in cui può eseguire codice JavaScript.

localStorage o sessionStorage non sono disponibili durante la pre-elaborazione. Se il componente tenta di interagire con l'archiviazione, viene generato un errore che spiega che non è possibile eseguire chiamate JavaScript di interoperabilità perché il componente viene prerenderizzato.

Un modo per risolvere l'errore consiste nel disabilitare il prerendering. Questa è in genere la scelta migliore se l'app usa pesantemente l'archiviazione basata su browser. La prerenderizzazione aggiunge complessità e non avvantaggia l'app perché l'app non può prerenderizzare alcun contenuto utile fino a quando localStorage o sessionStorage non sono disponibili.

Per disabilitare la prerendering, indicare la modalità di rendering con il prerender parametro impostato su false al componente di livello più alto nella gerarchia dei componenti dell'app che non è un componente radice.

Annotazioni

Rendere interattivo un componente radice, ad esempio il App componente, non è supportato. Di conseguenza, il prerendering non può essere disabilitato direttamente dal App componente.

Per le app basate sul modello di progetto Blazor Web App, il prerendering è generalmente disabilitato quando il componente Routes viene utilizzato nel componente App (Components/App.razor):

<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

Disabilita anche la prerendering per il componente HeadOutlet:

<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />

Per altre informazioni, vedere componenti Prerender ASP.NET CoreRazor.

Per disabilitare il pre-rendering, apri il file _Host.cshtml e modifica l'attributo render-mode del Tag Helper del componente in Server.

<component type="typeof(App)" render-mode="Server" />

Quando il prerendering è disattivato, il prerendering del contenuto <head> è disattivato.

Il prerendering potrebbe essere utile per altre pagine che non usano localStorage o sessionStorage. Per mantenere il prerendering, rinviare l'operazione di caricamento fino a quando il browser non è connesso al circuito. Di seguito è riportato un esempio per l'archiviazione di un valore del contatore:

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount;
    private bool isConnected;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedLocalStore.GetAsync<int>("count");
        currentCount = result.Success ? result.Value : 0;
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount = 0;
    private bool isConnected = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        currentCount = await ProtectedLocalStore.GetAsync<int>("count");
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}

Estrarre la gestione dello stato a un provider comune

Se molti componenti si basano sull'archiviazione basata su browser, l'implementazione del codice del provider di stato molte volte crea la duplicazione del codice. Un'opzione per evitare la duplicazione del codice consiste nel creare un componente padre del provider di stato che incapsula la logica del provider di stato. I componenti figlio possono funzionare con dati persistenti senza considerare il meccanismo di persistenza dello stato.

Nell'esempio seguente di un componente CounterStateProvider, i dati del contatore vengono salvati permanentemente in sessionStoragee gestisce la fase di caricamento non eseguendo il rendering del relativo contenuto figlio finché il caricamento dello stato non è completo.

Il componente CounterStateProvider gestisce il prerendering evitando di caricare lo stato finché non è stato eseguito il rendering del componente nel metodo del ciclo di vita OnAfterRenderAsync, che non viene eseguito durante il prerendering.

L'approccio in questa sezione non è in grado di attivare la ri-renderizzazione di più componenti sottoscritti nella stessa pagina. Se un componente iscritto modifica lo stato, viene rirenderizzato e può visualizzare lo stato aggiornato, ma un altro componente sulla stessa pagina che visualizza quello stato visualizza dati obsoleti finché non viene rirenderizzato nuovamente. Pertanto, l'approccio descritto in questa sezione è più adatto all'uso dello stato in un singolo componente nella pagina.

CounterStateProvider.razor:

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedSessionStore.GetAsync<int>("count");
        CurrentCount = result.Success ? result.Value : 0;
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}

Annotazioni

Per ulteriori informazioni su RenderFragment, vedi componenti di ASP.NET CoreRazor.

Per rendere lo stato accessibile a tutti i componenti di un'app, eseguire il wrapping del CounterStateProvider componente intorno a Router (<Router>...</Router>) nel Routes componente con rendering interattivo sul lato server globale (SSR interattivo).

Nel componente App (Components/App.razor):

<Routes @rendermode="InteractiveServer" />

Nel componente Routes (Components/Routes.razor):

Per utilizzare il componente CounterStateProvider, avvolgere un'istanza del componente intorno a qualsiasi altro componente che richiede l'accesso allo stato del contatore. Per rendere lo stato accessibile a tutti i componenti di un'app, avvolgere il componente CounterStateProvider attorno a Router nel componente App (App.razor):

<CounterStateProvider>
    <Router ...>
        ...
    </Router>
</CounterStateProvider>

Annotazioni

Con la versione di .NET 5.0.1 e per le versioni 5.x aggiuntive, il Router componente include il PreferExactMatches parametro impostato su @true. Per altre informazioni, vedere Eseguire la migrazione da ASP.NET Core 3.1 a .NET 5.

I componenti avvolti ricevono e possono modificare lo stato persistente del contatore. Il componente seguente Counter implementa il modello :

@page "/counter"

<p>Current count: <strong>@CounterStateProvider?.CurrentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>

@code {
    [CascadingParameter]
    private CounterStateProvider? CounterStateProvider { get; set; }

    private async Task IncrementCount()
    {
        if (CounterStateProvider is not null)
        {
            await CounterStateProvider.IncrementCount();
        }
    }
}

Il componente precedente non è necessario per interagire con ProtectedBrowserStorage, né gestisce una fase di "caricamento".

In generale, è consigliabile usare il modello componente genitore del provider di stato:

  • Per utilizzare lo stato in molti componenti.
  • Se è presente un solo oggetto di stato di primo livello da rendere persistente.

Per rendere persistenti molti oggetti di stato diversi e utilizzare subset diversi di oggetti in posizioni diverse, è preferibile evitare di rendere persistente lo stato a livello globale.