Elencare gli utenti loggati con Blazor Server

di Moreno Gentili, in ASP.NET Core,

Nel precedente script abbiamo visto come usare un CircuitHandler per conteggiare il numero di utenti collegati ad un'applicazione Blazor Server.
(https://www.aspitalia.com/script/1360/Conteggiare-Connessioni-SignalR-Aperte-Blazor-Server.aspx)

In questo script facciamo un passo in più e vediamo come il CircuitHandler possa essere usato per accedere al contesto HTTP, così da ottenere il nome dell'utente per aggiungerlo a un elenco di utenti loggati.

Il ciclo di vita di un Circuit

In precedenza abbiamo usato gli override OnCircuitOpenedAsync e OnCircuitClosedAsync per eseguire codice all'inizio e alla fine della vita di un Circuit, che è la rappresentazione logica di un client connesso all'applicazione.
Inoltre, possiamo usare gli override OnConnectionDownAsync e OnConnectionUpAsync che invece sono legati ad una connessione fisica WebSockets, come illustrato dall'immagine seguente. Eseguire codice in questi metodi è probabilmente più indicativo dell'effettivo stato di connessione di un utente.

Un CircuitHandler per tracciare gli utenti loggati

Il CircuitHandler che realizzeremo, a differenza di quello che abbiamo visto nello script precedente, ha una dipendenza da IServiceProvider. Grazie ad esso possiamo accedere ai servizi registrati per la dependency injection, tra cui l'IHttpContextAccessor che ci permette di ottenere il contesto HTTP e perciò il nome dell'utente loggato.
Vediamolo nel seguente esempio.

public class LoggedInUsersCircuitHandler : CircuitHandler, ILoggedInUserTracker
{
  //Usiamo un ConcurrentDictionary per mantenere un elenco degli utenti loggati in maniera thread-safe
  public ConcurrentDictionary<string, DateTime> loggedInUsers = new ConcurrentDictionary<string, DateTime>();
  
  //Questo CircuitHandler dipende da un IServiceProvider, ci servirà per ottenere un
  //riferimento al contesto HTTP corrente da cui otteniamo l'identità dell'utente
  private readonly IServiceProvider serviceProvider;

  public LoggedInUsersCircuitHandler(IServiceProvider serviceProvider)
  {
    this.serviceProvider = serviceProvider;
  }
  
  //Connessione al client stabilita
  public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
  {
    //Recuperiamo il suo username
    string username = GetAuthenticatedUserName();
    if (username != null)
    {
      //Se l'utente era loggato, allora aggiungiamo il suo username al ConcurrentDictionary
      if (loggedInUsers.TryAdd(username, DateTime.Now))
      {
        //E solleviamo un evento per notificare i sottoscrittori
        UserLoggedIn?.Invoke(this, username);
      }
    }
    
    //Invochiamo il metodo base del CircuitHandler
    return base.OnConnectionUpAsync(circuit, cancellationToken);
  }
  
  //Connessione al client chiusa
  public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
  {
    //Otteniamo lo username dell'utente
    string username = GetAuthenticatedUserName();
    if (username != null)
    {
      //Se era loggato, lo rimuoviamo dall'elenco
      if (loggedInUsers.TryRemove(username, out DateTime dateTime))
      {
        //E solleviamo un evento per notificare i sottoscrittori
        UserLoggedOut?.Invoke(this, username);
      }
    }
    //Invochiamo il metodo base
    return base.OnConnectionDownAsync(circuit, cancellationToken);
  }
  private string GetAuthenticatedUserName()
  {
    //Usando il ServiceProvider otteniamo il contesto HTTP corrente
    var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
    //E dal contesto HTTP otteniamo l'identità dell'utente
    IPrincipal user = httpContextAccessor.HttpContext.User;
    if (user.Identity.IsAuthenticated)
    {
      //Se era autenticato, restituiamo lo username
      return user.Identity.Name;
    }
    return null;
  }
  #region Implementazione di ILoggedInUserTracker
  public event EventHandler<string> UserLoggedIn;
  public event EventHandler<string> UserLoggedOut;
  public ICollection<string> CurrentLoggedInUsers => loggedInUsers.Keys;
  #endregion
}

Implementando l'interfaccia ILoggedInUserTracker possiamo esporre dei membri pubblici che saranno poi usati da un Razor Component per interagire con il CircuitHandler. Ecco di seguito la definizione di tale interfaccia.

public interface ILoggedInUserTracker
{
  event EventHandler<string> UserLoggedIn;
  event EventHandler<string> UserLoggedOut;
  ICollection<string> CurrentLoggedInUsers { get; }
}

Il CircuitHandler deve essere registrato così nella classe Startup, usando il ciclo di vita Singleton. Inoltre, facciamo in modo che la stessa istanza venga registrata per l'interfaccia ILoggedInUserTracker.

services.AddSingleton<CircuitHandler, LoggedInUsersCircuitHandler>();
//La stessa istanza viene riutilizzata anche quando si richiede il servizio ILoggedInUserTracker
services.AddSingleton<ILoggedInUserTracker>(provider => provider.GetService<CircuitHandler>() as ILoggedInUserTracker);
//Registriamo il servizio IHttpContextAccessor, che usiamo per recuperare l'identità dell'utente
services.AddHttpContextAccessor();

Visualizzare l'elenco degli utenti un Razor Component

Ora possiamo consumare il CircuitHandler da un Razor Component, così da visualizzare l'elenco degli utenti loggati. Inseriamo il seguente codice in un file .razor, ad esempio /Pages/Users.razor.

@page "/users"
@inject ILoggedInUserTracker userTracker
@implements IDisposable
<h1>Utenti attualmente loggati (@currentLoggedInUsers.Count)</h1>
<ul>  @foreach (var user in currentLoggedInUsers)
  {
    <li>@user</li>
  }
</ul>

@code {
  HashSet<string> currentLoggedInUsers;
  protected override void OnInitialized()
  {
    //Otteniamo il valore attuale dalla proprietà CurrentLoggedInUsers
    currentLoggedInUsers = userTracker.CurrentLoggedInUsers.ToHashSet();
    //E ci sottoscriviamo agli eventi per ricevere i futuri cambiamenti
    userTracker.UserLoggedIn += AddUserToList;
    userTracker.UserLoggedOut += RemoveUserFromList;
  }
  private void AddUserToList(object sender, string username)
  {
    //Usiamo InvokeAsync per far eseguire questo codice
    //al thread del renderer, altrimenti avremmo un'eccezione
    InvokeAsync(() => 
    {
      //Aggiungiamo alla lista il nome dell'utente loggato
      currentLoggedInUsers.Add(username);
      StateHasChanged();
    });
  }
  private void RemoveUserFromList(object sender, string username)
  {
    InvokeAsync(() => 
    {
      //Rimuoviamo l'utente dalla lista
      currentLoggedInUsers.Remove(username);
      StateHasChanged();
    });
  }
  public void Dispose()
  {
    //Rimuoviamo le sottoscrizioni quando
    //il Razor Component non serve più
    userTracker.UserLoggedIn -= AddUserToList;
    userTracker.UserLoggedOut -= RemoveUserFromList;
  }
}

Avviando l'applicazione in debug, vedremo l'elenco degli utenti loggati aggiornarsi in tempo reale. Se usiamo ASP.NET Core Identity, possiamo facilmente provarlo registrando vari utenti e facendo il login da browser diversi.


Dato che stiamo tracciando l'attività degli utenti, è sempre importante valutare se a livello legale sia necessario acquisire preventivamente il loro consenso, che può essere memorizzato su un cookie. Dovremo quindi subordinare il tracciamento dell'utente alla presenza di tale cookie, che possiamo verificare da httpContextAccessor.HttpContext.Request.Cookies.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi