Authentifizierung und Autorisierung in ASP.NET CoreSignalR

Authentifizieren von Benutzern, die eine Verbindung mit einem SignalR-Hub herstellen

SignalR kann mit ASP.NET Core-Authentifizierung verwendet werden, um jeder Verbindung einen Benutzer zuzuordnen. In einem Hub kann über die HubConnectionContext.User-Eigenschaft auf Authentifizierungsdaten zugegriffen werden. Authentifizierung ermöglicht es dem Hub, Methoden für alle Verbindungen aufzurufen, die einem Benutzer zugeordnet sind. Weitere Informationen finden Sie unter Verwalten von Benutzern und Gruppen in SignalR. Einem einzelnen Benutzer können mehrere Verbindungen zugeordnet werden.

Der folgende Code ist ein Beispiel, das SignalR und ASP.NET Core-Authentifizierung verwendet:

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

Hinweis

Wenn ein Token während der Lebensdauer einer Verbindung abläuft, funktioniert die Verbindung standardmäßig weiterhin. LongPolling- und ServerSentEvent-Verbindungen schlagen bei nachfolgenden Anforderungen fehl, wenn sie keine neuen Zugriffstoken senden. Damit Verbindungen geschlossen werden, wenn das Authentifizierungstoken abläuft, legen Sie CloseOnAuthenticationExpiration fest.

In einer browserbasierten App ermöglicht cookie-Authentifizierung, dass vorhandene Benutzeranmeldeinformationen automatisch in SignalR-Verbindungen einfließen. Bei Verwendung des Browserclients ist keine zusätzliche Konfiguration erforderlich. Wenn der Benutzer bei einer App angemeldet ist, erbt die SignalR-Verbindung automatisch diese Authentifizierung.

Cookies sind eine browserspezifische Methode zum Senden von Zugriffstoken, aber Nicht-Browserclients können sie senden. Bei Verwendung des .NET-Clients kann die Cookies-Eigenschaft im .WithUrl-Aufruf so konfiguriert werden, dass ein cookie bereitgestellt wird. Die Verwendung von cookie-Authentifizierung über den .NET-Client erfordert jedoch, dass die App eine API zum Austauschen von Authentifizierungsdaten für ein cookie bereitstellt.

Bearertokenauthentifizierung

Der Client kann ein Zugriffstoken bereitstellen, anstatt ein cookie zu verwenden. Der Server überprüft das Token und verwendet es zum Identifizieren des Benutzers. Diese Überprüfung erfolgt nur, wenn die Verbindung hergestellt wird. Während der Dauer der Verbindung führt der Server keine automatische erneute Überprüfung durch, um festzustellen, ob das Token widerrufen wurde.

Im JavaScript-Client kann das Token mithilfe der Option accessTokenFactory bereitgestellt werden.

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

Im .NET-Client gibt es eine ähnliche AccessTokenProvider-Eigenschaft, die zum Konfigurieren des Tokens verwendet werden kann:

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

Hinweis

Die bereitgestellte Zugriffstokenfunktion wird vor jeder HTTP-Anforderung aufgerufen, die von SignalR vorgenommen wird. Wenn das Token erneuert werden muss, um die Verbindung aktiv zu halten, führen Sie dies innerhalb dieser Funktion aus, und geben Sie das aktualisierte Token zurück. Das Token muss möglicherweise erneuert werden, damit es während der Verbindung nicht abläuft.

In Standardweb-APIs werden Bearertoken in einem HTTP-Header gesendet. SignalR kann diese Header jedoch nicht in Browsern festlegen, wenn gewisse Transporte verwendet werden. Bei Verwendung von WebSockets und Server-Sent Events wird das Token als Abfragezeichenfolgenparameter übertragen.

Integrierte JWT-Authentifizierung

Auf dem Server wird Bearertokenauthentifizierung unter Verwendung der JWT Bearer-Middleware konfiguriert:

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

Hinweis

Die Abfragezeichenfolge wird in Browsern verwendet, wenn eine Verbindung mit WebSockets und Server-Sent Events aufgrund von Browser-API-Einschränkungen hergestellt wird. Bei Verwendung von HTTPS werden Abfragezeichenfolgenwerte durch die TLS-Verbindung gesichert. Viele Server protokollieren jedoch Abfragezeichenfolgenwerte. Weitere Informationen finden Sie unter Sicherheitsüberlegungen zu ASP.NET CoreSignalR. SignalR verwendet Header, um Token in Umgebungen zu übertragen, die diese unterstützen (z. B. die .NET- und Java-Clients).

Identity Server-JWT-Authentifizierung

Wenn Sie Duende Identity Server verwenden, fügen Sie dem Projekt einen PostConfigureOptions<TOptions>-Dienst hinzu:

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;
                }
            }
        };
    }
}

Registrieren Sie den Dienst nach dem Hinzufügen von Diensten für die Authentifizierung (AddAuthentication) und dem Authentifizierungshandler für 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 im Vergleich zu Bearertoken

Cookies sind spezifisch für Browser. Das Senden von Token über andere Typen von Clients erhöht die Komplexität im Vergleich zum Versenden von Bearertoken. Cookie-Authentifizierung wird nur empfohlen, wenn die App nur Benutzer über den Browserclient authentifizieren muss. Bearertokenauthentifizierung ist der empfohlene Ansatz, wenn andere Clients als der Browserclient verwendet werden.

Windows-Authentifizierung

Wenn Windows-Authentifizierung in der App konfiguriert ist, kann SignalR diese Identität verwenden, um Hubs zu schützen. Fügen Sie jedoch einen benutzerdefinierten Benutzer-ID-Anbieter hinzu, um Nachrichten an einzelne Benutzer zu senden. Das Windows-Authentifizierungssystem stellt den Anspruch „Namenbezeichner“ nicht bereit. SignalR verwendet den Anspruch, um den Benutzernamen zu bestimmen.

Fügen Sie eine neue Klasse hinzu, die IUserIdProvider implementiert, und rufen Sie einen der Ansprüche vom Benutzer ab, um ihn als Bezeichner zu verwenden. Um beispielsweise den Anspruch „Name“ zu verwenden (der Windows-Benutzername im Format [Domain]/[Username]), erstellen Sie die folgende Klasse:

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

Verwenden Sie anstelle von ClaimTypes.Name einen beliebigen Wert aus User, z. B. den Windows-SID-Bezeichner usw.

Hinweis

Der ausgewählte Wert muss für alle Benutzer im System eindeutig sein. Andernfalls kann eine Nachricht, die für einen Benutzer bestimmt ist, ggf. an einen anderen Benutzer gesendet werden.

Registrieren Sie diese Komponente 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.

Im .NET-Client muss Windows-Authentifizierung durch Festlegen der UseDefaultCredentials-Eigenschaft aktiviert werden:

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

Windows-Authentifizierung wird in Microsoft Edge unterstützt, aber nicht in allen Browsern. In Chrome und Safari schlägt der Versuch beispielsweise fehl, Windows-Authentifizierung und WebSockets zu verwenden. Wenn Windows-Authentifizierung fehlschlägt, versucht der Client, auf andere Transporte zurückzugreifen, die möglicherweise funktionieren.

Verwenden von Ansprüchen zum Anpassen der Identitätsbehandlung

Eine App, die Benutzer authentifiziert, kann SignalR-Benutzer-IDs von Benutzeransprüchen ableiten. Implementieren Sie IUserIdProvider, und registrieren Sie die Implementierung, um anzugeben, wie SignalR Benutzer-IDs erstellt.

Der Beispielcode zeigt, wie Sie mithilfe von Ansprüchen die E-Mail-Adresse des Benutzers als identifizierende Eigenschaft auswählen.

Hinweis

Der ausgewählte Wert muss für alle Benutzer im System eindeutig sein. Andernfalls kann eine Nachricht, die für einen Benutzer bestimmt ist, ggf. an einen anderen Benutzer gesendet werden.

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

Die Kontoregistrierung fügt der ASP.NET-Identitätsdatenbank einen Anspruch mit dem Typ ClaimsTypes.Email hinzu.

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.

Registrieren Sie diese Komponente in Program.cs:

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

Autorisieren von Benutzern für den Zugriff auf Hubs und Hubmethoden

Standardmäßig können alle Methoden in einem Hub von einem nicht authentifizierten Benutzer aufgerufen werden. Wenden Sie das AuthorizeAttribute-Attribut auf den Hub an, um Authentifizierung zu verlangen:

[Authorize]
public class ChatHub: Hub
{
}

Die Konstruktorargumente und -eigenschaften des [Authorize]-Attributs können verwendet werden, um den Zugriff auf Benutzer zu beschränken, die bestimmten Autorisierungsrichtlinien entsprechen. Mit der benutzerdefinierten Autorisierungsrichtlinie namens MyAuthorizationPolicy können beispielsweise nur Benutzer, die dieser Richtlinie entsprechen, mit dem folgenden Code auf den Hub zugreifen:

[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.

Das [Authorize]-Attribut kann auf einzelne Hubmethoden angewendet werden. Wenn der aktuelle Benutzer nicht der für die Methode geltenden Richtlinie entspricht, wird ein Fehler an den Aufrufer zurückgegeben:

[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) ...
    }
}

Verwenden von Autorisierungshandlern zum Anpassen der Hubmethodenautorisierung

SignalR bietet eine benutzerdefinierte Ressource für Autorisierungshandler, wenn eine Hubmethode Autorisierung erfordert. Die Ressource ist eine Instanz von HubInvocationContext. HubInvocationContext enthält HubCallerContext, den Namen der aufgerufenen Hubmethode, sowie die Argumente für die Hubmethode.

Sehen Sie sich das Beispiel für einen Chatroom an, der mehreren Mitgliedern einer Organisation die Anmeldung über Microsoft Entra ID ermöglicht. Jede Person mit einem Microsoft-Konto kann sich beim Chat anmelden, aber nur Mitglieder der besitzenden Organisation sollte in der Lage sein, Benutzer zu sperren oder den Chatverlauf anderer Benutzer anzuzeigen. Darüber hinaus möchten Sie ggf. einige Funktionen für bestimmte Benutzer einschränken. Beachten Sie, dass DomainRestrictedRequirement als benutzerdefinierte IAuthorizationRequirement dient. Nachdem der HubInvocationContext-Ressourcenparameter übergeben wurde, kann die interne Logik den Kontext untersuchen, in dem der Hub aufgerufen wird, und Entscheidungen treffen, ob der Benutzer einzelne Hubmethoden ausführen darf:

[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));
    }
}

Fügen Sie in Program.cs die neue Richtlinie hinzu, und geben Sie dabei die benutzerdefinierte DomainRestrictedRequirement-Anforderung als Parameter an, um die DomainRestricted-Richtlinie zu erstellen:

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.

Im vorherigen Beispiel ist die DomainRestrictedRequirement-Klasse sowohl ein IAuthorizationRequirement als auch ein eigener AuthorizationHandler für diese Anforderung. Es ist akzeptabel, diese beiden Komponenten in separate Klassen aufzuteilen, um Aspekte voneinander zu trennen. Ein Vorteil des Ansatzes des Beispiels besteht darin, dass AuthorizationHandler beim Start nicht injiziert werden müssen, weil die Anforderung und der Handler ein und dasselbe sind.

Zusätzliche Ressourcen

Beispielcode anzeigen oder herunterladen (Vorgehensweise zum Herunterladen)

Authentifizieren von Benutzern, die eine Verbindung mit einem SignalR-Hub herstellen

SignalR kann mit ASP.NET Core-Authentifizierung verwendet werden, um jeder Verbindung einen Benutzer zuzuordnen. In einem Hub kann über die HubConnectionContext.User-Eigenschaft auf Authentifizierungsdaten zugegriffen werden. Authentifizierung ermöglicht es dem Hub, Methoden für alle Verbindungen aufzurufen, die einem Benutzer zugeordnet sind. Weitere Informationen finden Sie unter Verwalten von Benutzern und Gruppen in SignalR. Einem einzelnen Benutzer können mehrere Verbindungen zugeordnet werden.

Nachfolgend finden Sie ein Beispiel für Startup.Configure, das SignalR- und ASP.NET Core-Authentifizierung verwendet:

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?}");
    });
}

Hinweis

Wenn ein Token während der Lebensdauer einer Verbindung abläuft, funktioniert die Verbindung weiterhin. LongPolling- und ServerSentEvent-Verbindungen schlagen bei nachfolgenden Anforderungen fehl, wenn sie keine neuen Zugriffstoken senden.

In einer browserbasierten App ermöglicht cookie-Authentifizierung, dass Ihre vorhandenen Benutzeranmeldeinformationen automatisch in SignalR-Verbindungen einfließen. Bei Verwendung des Browserclients ist keine zusätzliche Konfiguration erforderlich. Wenn der Benutzer bei Ihrere App angemeldet ist, erbt die SignalR-Verbindung automatisch diese Authentifizierung.

Cookies sind eine browserspezifische Methode zum Senden von Zugriffstoken, aber Nicht-Browserclients können sie senden. Bei Verwendung des .NET-Clients kann die Cookies-Eigenschaft im .WithUrl-Aufruf so konfiguriert werden, dass ein cookie bereitgestellt wird. Die Verwendung von cookie-Authentifizierung über den .NET-Client erfordert jedoch, dass die App eine API zum Austauschen von Authentifizierungsdaten für ein cookie bereitstellt.

Bearertokenauthentifizierung

Der Client kann ein Zugriffstoken bereitstellen, anstatt ein cookie zu verwenden. Der Server überprüft das Token und verwendet es zum Identifizieren des Benutzers. Diese Überprüfung erfolgt nur, wenn die Verbindung hergestellt wird. Während der Dauer der Verbindung führt der Server keine automatische erneute Überprüfung durch, um festzustellen, ob das Token widerrufen wurde.

Im JavaScript-Client kann das Token mithilfe der Option accessTokenFactory bereitgestellt werden.

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

Im .NET-Client gibt es eine ähnliche AccessTokenProvider-Eigenschaft, die zum Konfigurieren des Tokens verwendet werden kann:

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

Hinweis

Die von Ihnen bereitgestellte Zugriffstokenfunktion wird vor jeder HTTP-Anforderung aufgerufen, die von SignalR vorgenommen wird. Wenn Sie das Token erneuern müssen, um die Verbindung aktiv zu halten (da sie während der Verbindung möglicherweise abläuft), führen Sie dies innerhalb dieser Funktion aus, und geben Sie das aktualisierte Token zurück.

In Standardweb-APIs werden Bearertoken in einem HTTP-Header gesendet. SignalR kann diese Header jedoch nicht in Browsern festlegen, wenn gewisse Transporte verwendet werden. Bei Verwendung von WebSockets und Server-Sent Events wird das Token als Abfragezeichenfolgenparameter übertragen.

Integrierte JWT-Authentifizierung

Auf dem Server wird Bearertokenauthentifizierung unter Verwendung der JWT Bearer-Middleware konfiguriert:

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. 
}

Wenn Sie möchten, dass Codekommentare in anderen Sprachen als Englisch angezeigt werden, informieren Sie uns in diesem GitHub-Issue.

Hinweis

Die Abfragezeichenfolge wird in Browsern verwendet, wenn eine Verbindung mit WebSockets und Server-Sent Events aufgrund von Browser-API-Einschränkungen hergestellt wird. Bei Verwendung von HTTPS werden Abfragezeichenfolgenwerte durch die TLS-Verbindung gesichert. Viele Server protokollieren jedoch Abfragezeichenfolgenwerte. Weitere Informationen finden Sie unter Sicherheitsüberlegungen zu ASP.NET CoreSignalR. SignalR verwendet Header, um Token in Umgebungen zu übertragen, die diese unterstützen (z. B. die .NET- und Java-Clients).

Identity Server-JWT-Authentifizierung

Wenn Sie Identity-Server verwenden, fügen Sie dem Projekt einen PostConfigureOptions<TOptions>-Dienst hinzu:

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;
                }
            }
        };
    }
}

Registrieren Sie den Dienst in Startup.ConfigureServices nach dem Hinzufügen von Diensten für die Authentifizierung (AddAuthentication) und dem Authentifizierungshandler für Identity-Server (AddIdentityServerJwt):

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

Cookies im Vergleich zu Bearertoken

Cookies sind spezifisch für Browser. Das Senden von Token über andere Typen von Clients erhöht die Komplexität im Vergleich zum Versenden von Bearertoken. cookie-Authentifizierung wird daher nur empfohlen, wenn die App Benutzer ausschließlich über den Browserclient authentifizieren muss. Bearertokenauthentifizierung ist der empfohlene Ansatz, wenn andere Clients als der Browserclient verwendet werden.

Windows-Authentifizierung

Wenn Windows-Authentifizierung in Ihrer App konfiguriert ist, kann SignalR diese Identität verwenden, um Hubs zu schützen. Um jedoch Nachrichten an einzelne Benutzer zu senden, müssen Sie einen benutzerdefinierten Benutzer-ID-Anbieter hinzufügen. Das Windows-Authentifizierungssystem stellt den Anspruch „Namenbezeichner“ nicht bereit. SignalR verwendet den Anspruch, um den Benutzernamen zu bestimmen.

Fügen Sie eine neue Klasse hinzu, die IUserIdProvider implementiert, und rufen Sie einen der Ansprüche vom Benutzer ab, um ihn als Bezeichner zu verwenden. Um beispielsweise den Anspruch „Name“ zu verwenden (der Windows-Benutzername im Format [Domain]\[Username]), erstellen Sie die folgende Klasse:

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

Anstelle von ClaimTypes.Name können Sie einen beliebigen Wert aus User verwenden (z. B. den Windows-SID-Bezeichner usw.).

Hinweis

Der von Ihnen ausgewählte Wert muss für alle Benutzer in Ihrem System eindeutig sein. Andernfalls kann eine Nachricht, die für einen Benutzer bestimmt ist, ggf. an einen anderen Benutzer gesendet werden.

Registrieren Sie diese Komponente in Ihrer Startup.ConfigureServices-Methode.

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

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

Im .NET-Client muss Windows-Authentifizierung durch Festlegen der UseDefaultCredentials-Eigenschaft aktiviert werden:

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

Windows-Authentifizierung wird in Internet Explorer und Microsoft Edge unterstützt, aber nicht in allen Browsern. In Chrome und Safari schlägt der Versuch beispielsweise fehl, Windows-Authentifizierung und WebSockets zu verwenden. Wenn Windows-Authentifizierung fehlschlägt, versucht der Client, auf andere Transporte zurückzugreifen, die möglicherweise funktionieren.

Verwenden von Ansprüchen zum Anpassen der Identitätsbehandlung

Eine App, die Benutzer authentifiziert, kann SignalR-Benutzer-IDs von Benutzeransprüchen ableiten. Implementieren Sie IUserIdProvider, und registrieren Sie die Implementierung, um anzugeben, wie SignalR Benutzer-IDs erstellt.

Der Beispielcode zeigt, wie Sie mithilfe von Ansprüchen die E-Mail-Adresse des Benutzers als identifizierende Eigenschaft auswählen können.

Hinweis

Der von Ihnen ausgewählte Wert muss für alle Benutzer in Ihrem System eindeutig sein. Andernfalls kann eine Nachricht, die für einen Benutzer bestimmt ist, ggf. an einen anderen Benutzer gesendet werden.

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

Die Kontoregistrierung fügt der ASP.NET-Identitätsdatenbank einen Anspruch mit dem Typ ClaimsTypes.Email hinzu.

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

Registrieren Sie diese Komponente in Ihren Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autorisieren von Benutzern für den Zugriff auf Hubs und Hubmethoden

Standardmäßig können alle Methoden in einem Hub von einem nicht authentifizierten Benutzer aufgerufen werden. Wenden Sie das AuthorizeAttribute-Attribut auf den Hub an, um Authentifizierung zu verlangen:

[Authorize]
public class ChatHub: Hub
{
}

Sie können die Konstruktorargumente und -eigenschaften des [Authorize]-Attributs verwenden, um den Zugriff auf Benutzer zu beschränken, die bestimmten Autorisierungsrichtlinien entsprechen. Wenn Sie z. B. eine benutzerdefinierte Autorisierungsrichtlinie namens MyAuthorizationPolicy besitzen, können Sie sicherstellen, dass nur Benutzer, die dieser Richtlinie entsprechen, mit dem folgenden Code auf den Hub zugreifen können:

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

Auf einzelne Hubmethoden kann auch das Attribut [Authorize] angewendet werden. Wenn der aktuelle Benutzer nicht der für die Methode geltenden Richtlinie entspricht, wird ein Fehler an den Aufrufer zurückgegeben:

[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) ...
    }
}

Verwenden von Autorisierungshandlern zum Anpassen der Hubmethodenautorisierung

SignalR bietet eine benutzerdefinierte Ressource für Autorisierungshandler, wenn eine Hubmethode Autorisierung erfordert. Die Ressource ist eine Instanz von HubInvocationContext. HubInvocationContext enthält HubCallerContext, den Namen der aufgerufenen Hubmethode, sowie die Argumente für die Hubmethode.

Sehen Sie sich das Beispiel für einen Chatroom an, der mehreren Mitgliedern einer Organisation die Anmeldung über Microsoft Entra ID ermöglicht. Jede Person mit einem Microsoft-Konto kann sich beim Chat anmelden, aber nur Mitglieder der besitzenden Organisation sollte in der Lage sein, Benutzer zu sperren oder den Chatverlauf anderer Benutzer anzuzeigen. Darüber hinaus können Sie bestimmte Funktionen für bestimmte Benutzer einschränken. Mit den aktualisierten Features in ASP.NET Core 3.0 ist dies durchaus möglich. Beachten Sie, dass DomainRestrictedRequirement als benutzerdefinierte IAuthorizationRequirement dient. Nachdem der HubInvocationContext-Ressourcenparameter übergeben wurde, kann die interne Logik den Kontext untersuchen, in dem der Hub aufgerufen wird, und Entscheidungen treffen, ob der Benutzer einzelne Hubmethoden ausführen darf.

[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));
    }
}

Fügen Sie in Startup.ConfigureServices die neue Richtlinie hinzu, und geben Sie dabei die benutzerdefinierte DomainRestrictedRequirement-Anforderung als Parameter an, um die DomainRestricted-Richtlinie zu erstellen.

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

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

Im vorherigen Beispiel ist die DomainRestrictedRequirement-Klasse sowohl ein IAuthorizationRequirement als auch ein eigener AuthorizationHandler für diese Anforderung. Es ist akzeptabel, diese beiden Komponenten in separate Klassen aufzuteilen, um Aspekte voneinander zu trennen. Ein Vorteil des Ansatzes des Beispiels besteht darin, dass AuthorizationHandler beim Start nicht injiziert werden müssen, weil die Anforderung und der Handler ein und dasselbe sind.

Zusätzliche Ressourcen