Uwierzytelnianie i autoryzacja w programie ASP.NET Core SignalR

Uwierzytelnianie użytkowników łączących się z koncentratorem SignalR

SignalRmożna używać z uwierzytelnianiem ASP.NET Core w celu skojarzenia użytkownika z każdym połączeniem. W centrum dostęp do danych uwierzytelniania można uzyskać z HubConnectionContext.User właściwości . Uwierzytelnianie umożliwia centrum wywoływanie metod na wszystkich połączeniach skojarzonych z użytkownikiem. Aby uzyskać więcej informacji, zobacz Zarządzanie użytkownikami i grupami w programie SignalR. Z jednym użytkownikiem może być skojarzonych wiele połączeń.

Poniższy kod to przykład użycia SignalR i ASP.NET uwierzytelniania podstawowego:

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

Uwaga

Jeśli token wygaśnie w okresie istnienia połączenia, domyślnie połączenie będzie nadal działać. LongPolling a ServerSentEvent połączenia kończą się niepowodzeniem w kolejnych żądaniach, jeśli nie wysyłają nowych tokenów dostępu. Aby połączenia zamykały się po wygaśnięciu tokenu uwierzytelniania, ustaw wartość CloseOnAuthenticationExpiration.

W aplikacji opartej na przeglądarce cookie uwierzytelnianie umożliwia automatyczne przepływy istniejących poświadczeń użytkownika do SignalR połączeń. W przypadku korzystania z klienta przeglądarki nie jest wymagana żadna dodatkowa konfiguracja. Jeśli użytkownik jest zalogowany do aplikacji, SignalR połączenie automatycznie dziedziczy to uwierzytelnianie.

Cookies to specyficzny dla przeglądarki sposób wysyłania tokenów dostępu, ale klienci niebędący przeglądarkami mogą je wysyłać. W przypadku korzystania z klienta Cookiesplatformy .NET właściwość można skonfigurować w wywołaniu .WithUrl w celu podania elementu cookie. Jednak użycie cookie uwierzytelniania z klienta platformy .NET wymaga, aby aplikacja udostępniała interfejs API do wymiany danych uwierzytelniania dla elementu cookie.

Uwierzytelnianie tokenu elementu nośnego

Klient może zapewnić token dostępu zamiast używać elementu cookie. Serwer weryfikuje token i używa go do identyfikowania użytkownika. Ta walidacja jest wykonywana tylko po nawiązaniu połączenia. Podczas okresu połączenia serwer nie jest automatycznie zmieniany w celu sprawdzenia odwołania tokenów.

W kliencie JavaScript token można podać przy użyciu opcji accessTokenFactory .

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

W kliencie platformy .NET istnieje podobna właściwość AccessTokenProvider , która może służyć do konfigurowania tokenu:

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

Uwaga

Podana funkcja tokenu dostępu jest wywoływana przed każdym żądaniem HTTP wykonanym przez SignalRprogram . Jeśli token musi zostać odnowiony, aby zachować aktywne połączenie, zrób to z poziomu tej funkcji i zwróć zaktualizowany token. Może być konieczne odnowienie tokenu, aby nie wygasało podczas połączenia.

W standardowych internetowych interfejsach API tokeny elementu nośnego są wysyłane w nagłówku HTTP. SignalR Nie można jednak ustawić tych nagłówków w przeglądarkach podczas korzystania z niektórych transportów. W przypadku korzystania z obiektów WebSocket i zdarzeń wysłanych przez serwer token jest przesyłany jako parametr ciągu zapytania.

Wbudowane uwierzytelnianie JWT

Na serwerze uwierzytelnianie tokenu elementu nośnego jest konfigurowane przy użyciu oprogramowania pośredniczącego elementu nośnego 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();

Uwaga

Ciąg zapytania jest używany w przeglądarkach podczas nawiązywania połączenia z elementami WebSocket i zdarzeniami wysłanymi przez serwer z powodu ograniczeń interfejsu API przeglądarki. W przypadku korzystania z protokołu HTTPS wartości parametrów zapytania są zabezpieczone przez połączenie TLS. Jednak wiele serwerów rejestruje wartości ciągu zapytania. Aby uzyskać więcej informacji, zobacz Zagadnienia dotyczące zabezpieczeń w programie ASP.NET Core SignalR. SignalR używa nagłówków do przesyłania tokenów w środowiskach, które je obsługują (takich jak klienci .NET i Java).

Identity Uwierzytelnianie JWT serwera

W przypadku korzystania z serwera Duende Identitydodaj usługę PostConfigureOptions<TOptions> do projektu:

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

Zarejestruj usługę po dodaniu usług do uwierzytelniania (AddAuthentication) i programu obsługi uwierzytelniania dla serwera Identity (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 a tokeny elementu nośnego

Cookies są specyficzne dla przeglądarek. Wysyłanie ich z innych rodzajów klientów zwiększa złożoność w porównaniu do wysyłania tokenów elementu nośnego. Cookie uwierzytelnianie nie jest zalecane, chyba że aplikacja musi uwierzytelniać użytkowników tylko z klienta przeglądarki. Uwierzytelnianie tokenu elementu nośnego jest zalecane w przypadku korzystania z klientów innych niż klient przeglądarki.

Uwierzytelnianie systemu Windows

Jeśli uwierzytelnianie systemu Windows jest skonfigurowane w aplikacji, SignalR może używać tej tożsamości do zabezpieczania centrów. Jednak aby wysyłać komunikaty do poszczególnych użytkowników, dodaj niestandardowego dostawcę identyfikatora użytkownika. System uwierzytelniania systemu Windows nie udostępnia oświadczenia "Identyfikator nazwy". SignalR używa oświadczenia, aby określić nazwę użytkownika.

Dodaj nową klasę, która implementuje IUserIdProvider i pobiera jeden z oświadczeń od użytkownika do użycia jako identyfikator. Aby na przykład użyć oświadczenia "Name" (czyli nazwy użytkownika systemu Windows w formularzu [Domain]/[Username]), utwórz następującą klasę:

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

ClaimTypes.NameZamiast , należy użyć dowolnej wartości z identyfikatora User, na przykład identyfikatora SID systemu Windows itp.

Uwaga

Wybrana wartość musi być unikatowa dla wszystkich użytkowników w systemie. W przeciwnym razie komunikat przeznaczony dla jednego użytkownika może trafić do innego użytkownika.

Zarejestruj ten składnik w pliku 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.

W kliencie .NET należy włączyć uwierzytelnianie systemu Windows, ustawiając UseDefaultCredentials właściwość :

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

Uwierzytelnianie systemu Windows jest obsługiwane w przeglądarce Microsoft Edge, ale nie we wszystkich przeglądarkach. Na przykład w przeglądarkach Chrome i Safari próba użycia uwierzytelniania systemu Windows i obiektów WebSocket kończy się niepowodzeniem. W przypadku niepowodzenia uwierzytelniania systemu Windows klient próbuje wrócić do innych transportów, które mogą działać.

Dostosowywanie obsługi tożsamości przy użyciu oświadczeń

Aplikacja, która uwierzytelnia użytkowników, może uzyskiwać SignalR identyfikatory użytkowników z oświadczeń użytkowników. Aby określić sposób SignalR tworzenia identyfikatorów użytkowników, zaimplementuj IUserIdProvider i zarejestruj implementację.

Przykładowy kod pokazuje, jak używać oświadczeń do wybierania adresu e-mail użytkownika jako właściwości identyfikującej.

Uwaga

Wybrana wartość musi być unikatowa dla wszystkich użytkowników w systemie. W przeciwnym razie komunikat przeznaczony dla jednego użytkownika może trafić do innego użytkownika.

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

Rejestracja konta dodaje oświadczenie z typem ClaimsTypes.Email do bazy danych tożsamości 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.

Zarejestruj ten składnik w pliku Program.cs:

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

Autoryzowanie użytkowników do uzyskiwania dostępu do centrów i metod centrów

Domyślnie wszystkie metody w centrum mogą być wywoływane przez nieuwierzytelnionego użytkownika. Aby wymagać uwierzytelniania, zastosuj AuthorizeAttribute atrybut do centrum:

[Authorize]
public class ChatHub: Hub
{
}

Argumenty i właściwości konstruktora atrybutu [Authorize] mogą służyć do ograniczania dostępu tylko do użytkowników pasujących do określonych zasad autoryzacji. Na przykład z niestandardowymi zasadami autoryzacji o nazwie MyAuthorizationPolicy, tylko użytkownicy pasujący do tych zasad mogą uzyskać dostęp do centrum przy użyciu następującego kodu:

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

Atrybut [Authorize] można zastosować do poszczególnych metod koncentratora. Jeśli bieżący użytkownik nie jest zgodny z zasadami zastosowanymi do metody, do wywołującego zostanie zwrócony błąd:

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

Używanie procedur obsługi autoryzacji do dostosowywania autoryzacji metody centrum

SignalR udostępnia niestandardowy zasób do procedur obsługi autoryzacji, gdy metoda centrum wymaga autoryzacji. Zasób jest wystąpieniem HubInvocationContextklasy . Element HubInvocationContext zawiera HubCallerContext, nazwę wywoływanej metody centrum oraz argumenty metody centrum.

Rozważmy przykład pokoju rozmów zezwalającej na logowanie wielu organizacji za pośrednictwem identyfikatora Firmy Microsoft Entra. Każda osoba mająca konto Microsoft może zalogować się do czatu, ale tylko członkowie organizacji będąca właścicielem powinni mieć możliwość zablokowania użytkowników lub wyświetlania historii czatów użytkowników. Ponadto możemy chcieć ograniczyć niektóre funkcje od określonych użytkowników. Zwróć uwagę, jak element DomainRestrictedRequirement służy jako niestandardowy IAuthorizationRequirementelement . Teraz, gdy HubInvocationContext parametr zasobu jest przekazywany, logika wewnętrzna może sprawdzić kontekst, w którym jest wywoływany centrum i podejmować decyzje dotyczące zezwalania użytkownikowi na wykonywanie poszczególnych metod centrum:

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

W Program.cspliku dodaj nowe zasady, podając DomainRestrictedRequirement niestandardowe wymaganie jako parametr do utworzenia DomainRestricted zasad:

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.

W poprzednim przykładzie DomainRestrictedRequirement klasa jest zarówno klasą, jak i własną AuthorizationHandlerIAuthorizationRequirement dla tego wymagania. Dopuszczalne jest podzielenie tych dwóch składników na oddzielne klasy w celu oddzielenia kwestii. Zaletą podejścia przykładu jest brak konieczności wstrzykiwania AuthorizationHandler podczas uruchamiania, ponieważ wymaganie i procedura obsługi są takie same.

Dodatkowe zasoby

Wyświetlanie lub pobieranie przykładowego kodu (jak pobrać)

Uwierzytelnianie użytkowników łączących się z koncentratorem SignalR

SignalRmożna używać z uwierzytelnianiem ASP.NET Core w celu skojarzenia użytkownika z każdym połączeniem. W centrum dostęp do danych uwierzytelniania można uzyskać z HubConnectionContext.User właściwości . Uwierzytelnianie umożliwia centrum wywoływanie metod na wszystkich połączeniach skojarzonych z użytkownikiem. Aby uzyskać więcej informacji, zobacz Zarządzanie użytkownikami i grupami w programie SignalR. Z jednym użytkownikiem może być skojarzonych wiele połączeń.

Poniżej przedstawiono przykład użycia SignalR i ASP.NET uwierzytelniania podstawowegoStartup.Configure:

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

Uwaga

Jeśli token wygaśnie w okresie istnienia połączenia, połączenie będzie nadal działać. LongPolling a ServerSentEvent połączenia kończą się niepowodzeniem w kolejnych żądaniach, jeśli nie wysyłają nowych tokenów dostępu.

W aplikacji opartej na przeglądarce cookie uwierzytelnianie umożliwia istniejącym poświadczeń użytkownika automatyczne przepływ do SignalR połączeń. W przypadku korzystania z klienta przeglądarki nie jest wymagana żadna dodatkowa konfiguracja. Jeśli użytkownik jest zalogowany do aplikacji, SignalR połączenie automatycznie dziedziczy to uwierzytelnianie.

Cookies to specyficzny dla przeglądarki sposób wysyłania tokenów dostępu, ale klienci niebędący przeglądarkami mogą je wysyłać. W przypadku korzystania z klienta Cookiesplatformy .NET właściwość można skonfigurować w wywołaniu .WithUrl w celu podania elementu cookie. Jednak użycie cookie uwierzytelniania z klienta platformy .NET wymaga, aby aplikacja udostępniała interfejs API do wymiany danych uwierzytelniania dla elementu cookie.

Uwierzytelnianie tokenu elementu nośnego

Klient może zapewnić token dostępu zamiast używać elementu cookie. Serwer weryfikuje token i używa go do identyfikowania użytkownika. Ta walidacja jest wykonywana tylko po nawiązaniu połączenia. Podczas okresu połączenia serwer nie jest automatycznie zmieniany w celu sprawdzenia odwołania tokenów.

W kliencie JavaScript token można podać przy użyciu opcji accessTokenFactory .

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

W kliencie platformy .NET istnieje podobna właściwość AccessTokenProvider , która może służyć do konfigurowania tokenu:

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

Uwaga

Udostępniona funkcja tokenu dostępu jest wywoływana przed każdym żądaniem HTTP wykonanym przez SignalRusługę . Jeśli musisz odnowić token, aby zachować aktywne połączenie (ponieważ może wygasać podczas połączenia), zrób to z poziomu tej funkcji i zwróć zaktualizowany token.

W standardowych internetowych interfejsach API tokeny elementu nośnego są wysyłane w nagłówku HTTP. SignalR Nie można jednak ustawić tych nagłówków w przeglądarkach podczas korzystania z niektórych transportów. W przypadku korzystania z obiektów WebSocket i zdarzeń wysłanych przez serwer token jest przesyłany jako parametr ciągu zapytania.

Wbudowane uwierzytelnianie JWT

Na serwerze uwierzytelnianie tokenu elementu nośnego jest konfigurowane przy użyciu oprogramowania pośredniczącego elementu nośnego 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. 
}

Jeśli chcesz zobaczyć komentarze kodu przetłumaczone na języki inne niż angielski, poinformuj nas o tym w tym problemie z dyskusją w usłudze GitHub.

Uwaga

Ciąg zapytania jest używany w przeglądarkach podczas nawiązywania połączenia z elementami WebSocket i zdarzeniami wysłanymi przez serwer z powodu ograniczeń interfejsu API przeglądarki. W przypadku korzystania z protokołu HTTPS wartości parametrów zapytania są zabezpieczone przez połączenie TLS. Jednak wiele serwerów rejestruje wartości ciągu zapytania. Aby uzyskać więcej informacji, zobacz Zagadnienia dotyczące zabezpieczeń w programie ASP.NET Core SignalR. SignalR używa nagłówków do przesyłania tokenów w środowiskach, które je obsługują (takich jak klienci .NET i Java).

Identity Uwierzytelnianie JWT serwera

W przypadku korzystania z serwera Identity dodaj usługę PostConfigureOptions<TOptions> do projektu:

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

Zarejestruj usługę w Startup.ConfigureServices usłudze po dodaniu usług do uwierzytelniania (AddAuthentication) i programu obsługi uwierzytelniania dla serwera Identity (AddIdentityServerJwt):

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

Cookies a tokeny elementu nośnego

Cookies są specyficzne dla przeglądarek. Wysyłanie ich z innych rodzajów klientów zwiększa złożoność w porównaniu do wysyłania tokenów elementu nośnego. W związku z tym uwierzytelnianie nie jest zalecane, cookie chyba że aplikacja musi uwierzytelniać użytkowników tylko z klienta przeglądarki. Uwierzytelnianie tokenu elementu nośnego jest zalecane w przypadku korzystania z klientów innych niż klient przeglądarki.

Uwierzytelnianie systemu Windows

Jeśli uwierzytelnianie systemu Windows jest skonfigurowane w aplikacji, SignalR może używać tej tożsamości do zabezpieczania centrów. Jednak aby wysyłać komunikaty do poszczególnych użytkowników, musisz dodać niestandardowego dostawcę identyfikatora użytkownika. System uwierzytelniania systemu Windows nie udostępnia oświadczenia "Identyfikator nazwy". SignalR używa oświadczenia, aby określić nazwę użytkownika.

Dodaj nową klasę, która implementuje IUserIdProvider i pobiera jeden z oświadczeń od użytkownika do użycia jako identyfikator. Aby na przykład użyć oświadczenia "Name" (czyli nazwy użytkownika systemu Windows w formularzu [Domain]\[Username]), utwórz następującą klasę:

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

ClaimTypes.NameZamiast , można użyć dowolnej wartości z identyfikatora User (na przykład identyfikatora SID systemu Windows itd.).

Uwaga

Wybrana wartość musi być unikatowa wśród wszystkich użytkowników w systemie. W przeciwnym razie komunikat przeznaczony dla jednego użytkownika może trafić do innego użytkownika.

Zarejestruj ten składnik w metodzie Startup.ConfigureServices .

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

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

W kliencie .NET należy włączyć uwierzytelnianie systemu Windows, ustawiając UseDefaultCredentials właściwość :

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

Uwierzytelnianie systemu Windows jest obsługiwane w programach Internet Explorer i Microsoft Edge, ale nie we wszystkich przeglądarkach. Na przykład w przeglądarkach Chrome i Safari próba użycia uwierzytelniania systemu Windows i obiektów WebSocket kończy się niepowodzeniem. W przypadku niepowodzenia uwierzytelniania systemu Windows klient próbuje wrócić do innych transportów, które mogą działać.

Dostosowywanie obsługi tożsamości przy użyciu oświadczeń

Aplikacja, która uwierzytelnia użytkowników, może uzyskiwać SignalR identyfikatory użytkowników z oświadczeń użytkowników. Aby określić sposób SignalR tworzenia identyfikatorów użytkowników, zaimplementuj IUserIdProvider i zarejestruj implementację.

Przykładowy kod pokazuje, jak używać oświadczeń, aby wybrać adres e-mail użytkownika jako właściwość identyfikującą.

Uwaga

Wybrana wartość musi być unikatowa wśród wszystkich użytkowników w systemie. W przeciwnym razie komunikat przeznaczony dla jednego użytkownika może trafić do innego użytkownika.

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

Rejestracja konta dodaje oświadczenie z typem ClaimsTypes.Email do bazy danych tożsamości 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));

Zarejestruj ten składnik w pliku Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Autoryzowanie użytkowników do uzyskiwania dostępu do centrów i metod centrów

Domyślnie wszystkie metody w centrum mogą być wywoływane przez nieuwierzytelnionego użytkownika. Aby wymagać uwierzytelniania, zastosuj AuthorizeAttribute atrybut do centrum:

[Authorize]
public class ChatHub: Hub
{
}

Można użyć argumentów konstruktora i właściwości atrybutu [Authorize] , aby ograniczyć dostęp tylko do użytkowników pasujących do określonych zasad autoryzacji. Jeśli na przykład masz niestandardowe zasady autoryzacji, MyAuthorizationPolicy możesz mieć pewność, że tylko użytkownicy pasujący do tych zasad mogą uzyskiwać dostęp do centrum przy użyciu następującego kodu:

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

Poszczególne metody koncentratora mogą również mieć [Authorize] zastosowany atrybut. Jeśli bieżący użytkownik nie jest zgodny z zasadami zastosowanymi do metody, do wywołującego zostanie zwrócony błąd:

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

Używanie procedur obsługi autoryzacji do dostosowywania autoryzacji metody centrum

SignalR udostępnia niestandardowy zasób do procedur obsługi autoryzacji, gdy metoda centrum wymaga autoryzacji. Zasób jest wystąpieniem HubInvocationContextklasy . Element HubInvocationContext zawiera HubCallerContext, nazwę wywoływanej metody centrum oraz argumenty metody centrum.

Rozważmy przykład pokoju rozmów zezwalającej na logowanie wielu organizacji za pośrednictwem identyfikatora Firmy Microsoft Entra. Każda osoba mająca konto Microsoft może zalogować się do czatu, ale tylko członkowie organizacji będąca właścicielem powinni mieć możliwość zablokowania użytkowników lub wyświetlania historii czatów użytkowników. Ponadto możemy chcieć ograniczyć niektóre funkcje od niektórych użytkowników. Dzięki zaktualizowanym funkcjom w programie ASP.NET Core 3.0 jest to całkowicie możliwe. Zwróć uwagę, jak element DomainRestrictedRequirement służy jako niestandardowy IAuthorizationRequirementelement . Teraz, gdy HubInvocationContext parametr zasobu jest przekazywany, logika wewnętrzna może sprawdzić kontekst, w którym jest wywoływany centrum, i podejmować decyzje dotyczące zezwalania użytkownikowi na wykonywanie poszczególnych metod centrum.

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

W Startup.ConfigureServicespliku dodaj nowe zasady, podając DomainRestrictedRequirement niestandardowe wymaganie jako parametr do utworzenia DomainRestricted zasad.

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

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

W poprzednim przykładzie DomainRestrictedRequirement klasa jest zarówno klasą, jak i własną AuthorizationHandlerIAuthorizationRequirement dla tego wymagania. Dopuszczalne jest podzielenie tych dwóch składników na oddzielne klasy w celu oddzielenia kwestii. Zaletą podejścia przykładu jest brak konieczności wstrzykiwania AuthorizationHandler podczas uruchamiania, ponieważ wymaganie i procedura obsługi są takie same.

Dodatkowe zasoby