Autenticazione e autorizzazione in ASP.NET Core SignalR

Autenticare gli utenti che si connettono a un SignalR hub

SignalR può essere usato con ASP.NET'autenticazione core per associare un utente a ogni connessione. In un hub è possibile accedere ai dati di autenticazione dalla HubConnectionContext.User proprietà . L'autenticazione consente all'hub di chiamare i metodi su tutte le connessioni associate a un utente. Per altre informazioni, vedere Gestire utenti e gruppi in SignalR. È possibile associare più connessioni a un singolo utente.

Il codice seguente è un esempio che usa SignalR e ASP.NET'autenticazione core:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

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>();
builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

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

app.MapRazorPages();
app.MapHub<ChatHub>("/chat");

app.Run();

Nota

Se un token scade durante la durata di una connessione, per impostazione predefinita la connessione continua a funzionare. LongPolling e ServerSentEvent le connessioni hanno esito negativo nelle richieste successive se non inviano nuovi token di accesso. Per chiudere le connessioni alla scadenza del token di autenticazione, impostare CloseOnAuthenticationExpiration.

In un'app basata su browser, cookie l'autenticazione consente alle credenziali utente esistenti di passare automaticamente alle SignalR connessioni. Quando si usa il client del browser, non è necessaria alcuna configurazione aggiuntiva. Se l'utente ha eseguito l'accesso a un'app, la SignalR connessione eredita automaticamente questa autenticazione.

Cookies è un modo specifico del browser per inviare i token di accesso, ma i client non browser possono inviarli. Quando si usa il client .NET, la Cookies proprietà può essere configurata nella .WithUrl chiamata per fornire un oggetto cookie. Tuttavia, l'uso cookie dell'autenticazione dal client .NET richiede all'app di fornire un'API per scambiare i dati di autenticazione per un oggetto cookie.

Autenticazione del token di connessione

Il client può fornire un token di accesso anziché usare un oggetto cookie. Il server convalida il token e lo usa per identificare l'utente. Questa convalida viene eseguita solo quando viene stabilita la connessione. Durante la durata della connessione, il server non riconvalida automaticamente per verificare la revoca del token.

Nel client JavaScript il token può essere fornito usando l'opzione accessTokenFactory .

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

Nel client .NET è disponibile una proprietà AccessTokenProvider simile che può essere usata per configurare il token:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Nota

La funzione del token di accesso fornita viene chiamata prima di ogni richiesta HTTP effettuata da SignalR. Se il token deve essere rinnovato per mantenere attiva la connessione, eseguire questa operazione dall'interno di questa funzione e restituire il token aggiornato. Potrebbe essere necessario rinnovare il token in modo che non scada durante la connessione.

Nelle API Web standard, i token di connessione vengono inviati in un'intestazione HTTP. Tuttavia, SignalR non è possibile impostare queste intestazioni nei browser quando si usano alcuni trasporti. Quando si usano WebSocket ed eventi inviati dal server, il token viene trasmesso come parametro di stringa di query.

Autenticazione JWT predefinita

Nel server l'autenticazione del token di connessione viene configurata usando il middleware bearer JWT:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddAuthentication(options =>
{
    // Identity made Cookie authentication the default.
    // However, we want JWT Bearer Auth to be the default.
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
  {
      // Configure the Authority to the expected value for
      // the authentication provider. This ensures the token
      // is appropriately validated.
      options.Authority = "Authority URL"; // TODO: Update URL

      // We have to hook the OnMessageReceived event in order to
      // allow the JWT authentication handler to read the access
      // token from the query string when a WebSocket or 
      // Server-Sent Events request comes in.

      // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
      // due to a limitation in Browser APIs. We restrict it to only calls to the
      // SignalR hub in this code.
      // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
      // for more information about security considerations when using
      // the query string to transmit the access token.
      options.Events = new JwtBearerEvents
      {
          OnMessageReceived = context =>
          {
              var accessToken = context.Request.Query["access_token"];

              // If the request is for our hub...
              var path = context.HttpContext.Request.Path;
              if (!string.IsNullOrEmpty(accessToken) &&
                  (path.StartsWithSegments("/hubs/chat")))
              {
                  // Read the token out of the query string
                  context.Token = accessToken;
              }
              return Task.CompletedTask;
          }
      };
  });

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token 
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages 
// intended for a different user!
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

// Change to use email as the user identifier for SignalR
// builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

// WARNING: use *either* the NameUserIdProvider *or* the 
// EmailBasedUserIdProvider, but do not use both. 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

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

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

Nota

La stringa di query viene usata nei browser durante la connessione con WebSocket ed eventi inviati dal server a causa delle limitazioni dell'API del browser. Quando si usa HTTPS, i valori delle stringhe di query vengono protetti dalla connessione TLS. Tuttavia, molti server registrano valori di stringa di query. Per altre informazioni, vedere Considerazioni sulla sicurezza in ASP.NET Core SignalR. SignalR usa le intestazioni per trasmettere i token in ambienti che li supportano, ad esempio i client .NET e Java.

Identity Autenticazione JWT server

Quando si usa Duende IdentityServer, aggiungere un PostConfigureOptions<TOptions> servizio al progetto:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Registrare il servizio dopo l'aggiunta di servizi per l'autenticazione (AddAuthentication) e il gestore di autenticazione per Identity Server (AddIdentityServerJwt):

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
        ConfigureJwtBearerOptions>());

builder.Services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

Cookies vs. token di connessione

Cookies sono specifici dei browser. L'invio da altri tipi di client aggiunge complessità rispetto all'invio di token di connessione. Cookie l'autenticazione non è consigliata a meno che l'app non debba solo autenticare gli utenti dal client del browser. L'autenticazione del token di connessione è l'approccio consigliato quando si usano client diversi dal client del browser.

Autenticazione di Windows

Se autenticazione di Windows è configurato nell'app, SignalR può usare tale identità per proteggere gli hub. Tuttavia, per inviare messaggi a singoli utenti, aggiungere un provider di ID utente personalizzato. Il sistema autenticazione di Windows non fornisce l'attestazione "Identificatore nome". SignalR usa l'attestazione per determinare il nome utente.

Aggiungere una nuova classe che implementa e recuperare una delle attestazioni IUserIdProvider dall'utente da usare come identificatore. Ad esempio, per usare l'attestazione "Name" (ovvero il nome utente di Windows nel formato [Domain]/[Username]), creare la classe seguente:

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Invece di ClaimTypes.Name, usare qualsiasi valore di User, ad esempio l'identificatore SID di Windows e così via.

Nota

Il valore scelto deve essere univoco tra tutti gli utenti del sistema. In caso contrario, un messaggio destinato a un utente potrebbe terminare con un utente diverso.

Registrare questo componente in Program.cs:

using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.SignalR;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();

services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

var app = builder.Build();

// Code removed for brevity.

Nel client .NET l'autenticazione di Windows deve essere abilitata impostando la UseDefaultCredentials proprietà :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

autenticazione di Windows è supportato in Microsoft Edge, ma non in tutti i browser. Ad esempio, in Chrome e Safari, il tentativo di usare autenticazione di Windows e WebSocket ha esito negativo. Quando autenticazione di Windows ha esito negativo, il client tenta di eseguire il fallback ad altri trasporti che potrebbero funzionare.

Usare le attestazioni per personalizzare la gestione delle identità

Un'app che autentica gli utenti può derivare SignalR gli ID utente dalle attestazioni utente. Per specificare la modalità SignalR di creazione degli ID utente, implementare e registrare IUserIdProvider l'implementazione.

Il codice di esempio illustra come usare le attestazioni per selezionare l'indirizzo di posta elettronica dell'utente come proprietà di identificazione.

Nota

Il valore scelto deve essere univoco tra tutti gli utenti del sistema. In caso contrario, un messaggio destinato a un utente potrebbe terminare con un utente diverso.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value!;
    }
}

La registrazione dell'account aggiunge un'attestazione con tipo ClaimsTypes.Email al database identity ASP.NET.

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
        var result = await _userManager.CreateAsync(user, Input.Password);

        // Add the email claim and value for this user.
        await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

        // Remaining code removed for brevity.

Registrare questo componente in Program.cs:

builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autorizzare gli utenti ad accedere a hub e metodi hub

Per impostazione predefinita, tutti i metodi in un hub possono essere chiamati da un utente non autenticato. Per richiedere l'autenticazione, applicare l'attributo AuthorizeAttribute all'hub:

[Authorize]
public class ChatHub: Hub
{
}

Gli argomenti e le proprietà del costruttore dell'attributo possono essere usati per limitare l'accesso [Authorize] solo agli utenti che corrispondono a criteri di autorizzazione specifici. Ad esempio, con i criteri di autorizzazione personalizzati denominati MyAuthorizationPolicy, solo gli utenti corrispondenti a tale criterio possono accedere all'hub usando il codice seguente:

[Authorize("MyAuthorizationPolicy")]
public class ChatPolicyHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Clients.All.SendAsync("ReceiveSystemMessage", 
                                    $"{Context.UserIdentifier} joined.");
        await base.OnConnectedAsync();
    }
    // Code removed for brevity.

L'attributo [Authorize] può essere applicato ai singoli metodi hub. Se l'utente corrente non corrisponde al criterio applicato al metodo, viene restituito un errore al chiamante:

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}

Usare i gestori di autorizzazione per personalizzare l'autorizzazione del metodo hub

SignalR fornisce una risorsa personalizzata ai gestori di autorizzazione quando un metodo hub richiede l'autorizzazione. La risorsa è un'istanza di HubInvocationContext. HubInvocationContext include , HubCallerContextil nome del metodo hub richiamato e gli argomenti per il metodo hub.

Si consideri l'esempio di una chat room che consente l'accesso a più organizzazioni tramite Microsoft Entra ID. Chiunque abbia un account Microsoft può accedere alla chat, ma solo i membri dell'organizzazione proprietaria devono essere in grado di vietare gli utenti o visualizzare le cronologie delle chat degli utenti. Inoltre, potrebbe essere necessario limitare alcune funzionalità da utenti specifici. Si noti come funge DomainRestrictedRequirement da oggetto personalizzato IAuthorizationRequirement. Ora che il HubInvocationContext parametro della risorsa viene passato, la logica interna può esaminare il contesto in cui viene chiamato l'hub e prendere decisioni su come consentire all'utente di eseguire singoli metodi hub:

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

namespace SignalRAuthenticationSample;

public class DomainRestrictedRequirement :
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement,
        HubInvocationContext resource)
    {
        if (context.User.Identity != null &&
          !string.IsNullOrEmpty(context.User.Identity.Name) && 
          IsUserAllowedToDoThis(resource.HubMethodName,
                               context.User.Identity.Name) &&
          context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
                context.Succeed(requirement);
            
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") &&
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

In Program.csaggiungere il nuovo criterio specificando il requisito personalizzato DomainRestrictedRequirement come parametro per creare il DomainRestricted criterio:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var services = builder.Services;

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
services.AddDatabaseDeveloperPageExceptionFilter();

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

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

services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.

Nell'esempio precedente la DomainRestrictedRequirement classe è sia un IAuthorizationRequirement oggetto che il proprio AuthorizationHandler per tale requisito. È accettabile suddividere questi due componenti in classi separate per separare le problematiche. Un vantaggio dell'approccio dell'esempio non è necessario inserire durante l'avvio AuthorizationHandler , perché il requisito e il gestore sono la stessa cosa.

Risorse aggiuntive

Visualizzare o scaricare il codice di esempio (come scaricare)

Autenticare gli utenti che si connettono a un SignalR hub

SignalR può essere usato con ASP.NET'autenticazione core per associare un utente a ogni connessione. In un hub è possibile accedere ai dati di autenticazione dalla HubConnectionContext.User proprietà . L'autenticazione consente all'hub di chiamare i metodi su tutte le connessioni associate a un utente. Per altre informazioni, vedere Gestire utenti e gruppi in SignalR. È possibile associare più connessioni a un singolo utente.

Di seguito è riportato un esempio di Startup.Configure che usa SignalR e ASP.NET'autenticazione core:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseRouting();

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

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}

Nota

Se un token scade durante la durata di una connessione, la connessione continua a funzionare. LongPolling e ServerSentEvent le connessioni hanno esito negativo nelle richieste successive se non inviano nuovi token di accesso.

In un'app basata su browser, cookie l'autenticazione consente alle credenziali utente esistenti di passare automaticamente alle SignalR connessioni. Quando si usa il client del browser, non è necessaria alcuna configurazione aggiuntiva. Se l'utente ha eseguito l'accesso all'app, la SignalR connessione eredita automaticamente questa autenticazione.

Cookies è un modo specifico del browser per inviare i token di accesso, ma i client non browser possono inviarli. Quando si usa il client .NET, la Cookies proprietà può essere configurata nella .WithUrl chiamata per fornire un oggetto cookie. Tuttavia, l'uso cookie dell'autenticazione dal client .NET richiede all'app di fornire un'API per scambiare i dati di autenticazione per un oggetto cookie.

Autenticazione del token di connessione

Il client può fornire un token di accesso anziché usare un oggetto cookie. Il server convalida il token e lo usa per identificare l'utente. Questa convalida viene eseguita solo quando viene stabilita la connessione. Durante la durata della connessione, il server non riconvalida automaticamente per verificare la revoca del token.

Nel client JavaScript il token può essere fornito usando l'opzione accessTokenFactory .

// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();

Nel client .NET è disponibile una proprietà AccessTokenProvider simile che può essere usata per configurare il token:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    { 
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();

Nota

La funzione token di accesso specificata viene chiamata prima di ogni richiesta HTTP effettuata da SignalR. Se è necessario rinnovare il token per mantenere attiva la connessione (perché potrebbe scadere durante la connessione), eseguire questa operazione dall'interno di questa funzione e restituire il token aggiornato.

Nelle API Web standard, i token di connessione vengono inviati in un'intestazione HTTP. Tuttavia, SignalR non è possibile impostare queste intestazioni nei browser quando si usano alcuni trasporti. Quando si usano WebSocket ed eventi inviati dal server, il token viene trasmesso come parametro di stringa di query.

Autenticazione JWT predefinita

Nel server l'autenticazione del token di connessione viene configurata usando il middleware bearer JWT:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication(options =>
        {
            // Identity made Cookie authentication the default.
            // However, we want JWT Bearer Auth to be the default.
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            // Configure the Authority to the expected value for your authentication provider
            // This ensures the token is appropriately validated
            options.Authority = /* TODO: Insert Authority URL here */;

            // We have to hook the OnMessageReceived event in order to
            // allow the JWT authentication handler to read the access
            // token from the query string when a WebSocket or 
            // Server-Sent Events request comes in.

            // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
            // due to a limitation in Browser APIs. We restrict it to only calls to the
            // SignalR hub in this code.
            // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
            // for more information about security considerations when using
            // the query string to transmit the access token.
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    // If the request is for our hub...
                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) &&
                        (path.StartsWithSegments("/hubs/chat")))
                    {
                        // Read the token out of the query string
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddSignalR();

    // Change to use Name as the user identifier for SignalR
    // WARNING: This requires that the source of your JWT token 
    // ensures that the Name claim is unique!
    // If the Name claim isn't unique, users could receive messages 
    // intended for a different user!
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

    // Change to use email as the user identifier for SignalR
    // services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

    // WARNING: use *either* the NameUserIdProvider *or* the 
    // EmailBasedUserIdProvider, but do not use both. 
}

Per visualizzare i commenti del codice tradotti in lingue diverse dall'inglese, segnalarlo in questo problema di discussione su GitHub.

Nota

La stringa di query viene usata nei browser durante la connessione con WebSocket ed eventi inviati dal server a causa delle limitazioni dell'API del browser. Quando si usa HTTPS, i valori delle stringhe di query vengono protetti dalla connessione TLS. Tuttavia, molti server registrano valori di stringa di query. Per altre informazioni, vedere Considerazioni sulla sicurezza in ASP.NET Core SignalR. SignalR usa le intestazioni per trasmettere i token in ambienti che li supportano, ad esempio i client .NET e Java.

Identity Autenticazione JWT server

Quando si usa Identity Server, aggiungere un PostConfigureOptions<TOptions> servizio al progetto:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) && 
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}

Registrare il servizio in Startup.ConfigureServices dopo l'aggiunta di servizi per l'autenticazione (AddAuthentication) e il gestore di autenticazione per Identity Server (AddIdentityServerJwt):

services.AddAuthentication()
    .AddIdentityServerJwt();
services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, 
        ConfigureJwtBearerOptions>());

Cookies vs. token di connessione

Cookies sono specifici dei browser. L'invio da altri tipi di client aggiunge complessità rispetto all'invio di token di connessione. Di conseguenza, cookie l'autenticazione non è consigliata a meno che l'app non debba autenticare solo gli utenti dal client del browser. L'autenticazione del token di connessione è l'approccio consigliato quando si usano client diversi dal client del browser.

Autenticazione di Windows

Se autenticazione di Windows è configurato nell'app, SignalR può usare tale identità per proteggere gli hub. Tuttavia, per inviare messaggi a singoli utenti, è necessario aggiungere un provider di ID utente personalizzato. Il sistema autenticazione di Windows non fornisce l'attestazione "Identificatore nome". SignalR usa l'attestazione per determinare il nome utente.

Aggiungere una nuova classe che implementa e recuperare una delle attestazioni IUserIdProvider dall'utente da usare come identificatore. Ad esempio, per usare l'attestazione "Name" (ovvero il nome utente di Windows nel formato [Domain]\[Username]), creare la classe seguente:

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

Invece di ClaimTypes.Name, è possibile usare qualsiasi valore da User , ad esempio l'identificatore SID di Windows e così via.

Nota

Il valore scelto deve essere univoco tra tutti gli utenti del sistema. In caso contrario, un messaggio destinato a un utente potrebbe terminare con un utente diverso.

Registrare questo componente nel Startup.ConfigureServices metodo.

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services.AddSignalR();
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}

Nel client .NET l'autenticazione di Windows deve essere abilitata impostando la UseDefaultCredentials proprietà :

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();

autenticazione di Windows è supportato in Internet Explorer e Microsoft Edge, ma non in tutti i browser. Ad esempio, in Chrome e Safari, il tentativo di usare autenticazione di Windows e WebSocket ha esito negativo. Quando autenticazione di Windows ha esito negativo, il client tenta di eseguire il fallback ad altri trasporti che potrebbero funzionare.

Usare le attestazioni per personalizzare la gestione delle identità

Un'app che autentica gli utenti può derivare SignalR gli ID utente dalle attestazioni utente. Per specificare la modalità SignalR di creazione degli ID utente, implementare e registrare IUserIdProvider l'implementazione.

Il codice di esempio illustra come usare le attestazioni per selezionare l'indirizzo di posta elettronica dell'utente come proprietà di identificazione.

Nota

Il valore scelto deve essere univoco tra tutti gli utenti del sistema. In caso contrario, un messaggio destinato a un utente potrebbe terminare con un utente diverso.

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}

La registrazione dell'account aggiunge un'attestazione con tipo ClaimsTypes.Email al database identity ASP.NET.

// create a new user
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);

// add the email claim and value for this user
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

Registrare questo componente in Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autorizzare gli utenti ad accedere a hub e metodi hub

Per impostazione predefinita, tutti i metodi in un hub possono essere chiamati da un utente non autenticato. Per richiedere l'autenticazione, applicare l'attributo AuthorizeAttribute all'hub:

[Authorize]
public class ChatHub: Hub
{
}

È possibile usare gli argomenti e le proprietà del costruttore dell'attributo per limitare l'accesso [Authorize] solo agli utenti che corrispondono a criteri di autorizzazione specifici. Ad esempio, se si dispone di un criterio di autorizzazione personalizzato denominato MyAuthorizationPolicy , è possibile assicurarsi che solo gli utenti corrispondenti a tale criterio possano accedere all'hub usando il codice seguente:

[Authorize("MyAuthorizationPolicy")]
public class ChatHub : Hub
{
}

Anche i singoli metodi hub possono avere l'attributo [Authorize] applicato. Se l'utente corrente non corrisponde al criterio applicato al metodo, viene restituito un errore al chiamante:

[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}

Usare i gestori di autorizzazione per personalizzare l'autorizzazione del metodo hub

SignalR fornisce una risorsa personalizzata ai gestori di autorizzazione quando un metodo hub richiede l'autorizzazione. La risorsa è un'istanza di HubInvocationContext. HubInvocationContext include , HubCallerContextil nome del metodo hub richiamato e gli argomenti per il metodo hub.

Si consideri l'esempio di una chat room che consente l'accesso a più organizzazioni tramite Microsoft Entra ID. Chiunque abbia un account Microsoft può accedere alla chat, ma solo i membri dell'organizzazione proprietaria devono essere in grado di vietare gli utenti o visualizzare le cronologie delle chat degli utenti. Inoltre, potrebbe essere necessario limitare determinate funzionalità a determinati utenti. Usando le funzionalità aggiornate in ASP.NET Core 3.0, questo è completamente possibile. Si noti come funge DomainRestrictedRequirement da oggetto personalizzato IAuthorizationRequirement. Ora che il HubInvocationContext parametro della risorsa viene passato, la logica interna può esaminare il contesto in cui viene chiamato l'hub e prendere decisioni su come consentire all'utente di eseguire singoli metodi hub.

[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

public class DomainRestrictedRequirement : 
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>, 
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement, 
        HubInvocationContext resource)
    {
        if (IsUserAllowedToDoThis(resource.HubMethodName, context.User.Identity.Name) && 
            context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") && 
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}

In Startup.ConfigureServicesaggiungere il nuovo criterio, specificando il requisito personalizzato DomainRestrictedRequirement come parametro per creare il DomainRestricted criterio.

public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

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

Nell'esempio precedente la DomainRestrictedRequirement classe è sia un IAuthorizationRequirement oggetto che il proprio AuthorizationHandler per tale requisito. È accettabile suddividere questi due componenti in classi separate per separare le problematiche. Un vantaggio dell'approccio dell'esempio non è necessario inserire durante l'avvio AuthorizationHandler , perché il requisito e il gestore sono la stessa cosa.

Risorse aggiuntive