Проверка подлинности и авторизация в ASP.NET Core SignalR

В этой статье описывается проверка подлинности и авторизация пользователей в приложениях ASP.NET Core с помощью SignalR.

Проверка подлинности пользователей, подключающихся к концентратору SignalR

SignalR можно использовать с проверкой подлинности ASP.NET Core для ассоциации пользователя с каждым подключением. Данные аутентификации в узле можно получить из свойства HubConnectionContext.User. Аутентификация позволяет концентратору вызывать методы на всех подключениях, связанных с пользователем. Дополнительные сведения см. в разделе "Управление пользователями и группами".SignalR Несколько подключений могут быть связаны с одним пользователем.

Следующий код является примером использования SignalR и аутентификации ASP.NET 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();

Note

Если токен истекает в течение срока существования подключения, по умолчанию подключение продолжает работать. LongPolling и ServerSentEvents подключения завершаются сбоем при последующих запросах, если они не отправляют новые маркеры доступа. Чтобы подключения закрывались после истечения срока действия маркера аутентификации, задайте параметр CloseOnAuthenticationExpiration.

Изменения пользователей и ролей во время существования подключения

SignalR записывает прошедшего проверку подлинности пользователя при установке подключения и кэширует его в течение всего времени существования подключения. Кэшированный субъект предоставляется методам концентратора через Context.User свойство (HubCallerContext.User) и используется для авторизации вызовов методов концентратора. SignalR не выполняет автоматическое повторную проверку подлинности пользователя во время жизни подключения независимо от схемы проверки подлинности. Это поведение относится ко всем схемам, включая аутентификацию cookie и аутентификацию с помощью токена предъявителя.

Изменения сведений о пользователе, его ролей или утверждений, которые происходят после установления соединения, не отражаются в существующем соединении. Это поведение сохраняется, даже когда базовый транспорт создает новые HTTP-запросы, например в транспортах LongPolling и ServerSentEvents. Хотя промежуточное ПО аутентификации ASP.NET Core повторно аутентифицирует каждый из этих HTTP-запросов, SignalR продолжает использовать объект principal, сохранённый в кэше при установлении соединения.

Рассмотрим следующий сценарий.

  • Приложение использует аутентификацию cookie и транспорт LongPolling. То же поведение относится к аутентификации с использованием токена-носителя и транспорту ServerSentEvents.
  • Пользователь вошёл в систему с ролью Editor и у него открыто соединение SignalR.
  • Приложение удаляет у этого пользователя роль Editor.

При следующем запросе long-poll промежуточное ПО аутентификации может аутентифицировать пользователя без роли Editor. Например, это может произойти, если роли загружаются из хранилища данных или cookie обновляются. Однако концентратор продолжает авторизовать вызовы метода концентратора пользователя [Authorize(Roles = "Editor")] , так как Context.User (HubCallerContext.User) по-прежнему хранит субъект, который был кэширован до удаления роли. Пользователь может продолжать вызывать методы концентратора, доступные только через Editor, пока соединение не будет закрыто.

Чтобы применить обновленную авторизацию для активного подключения, выполните один из следующих подходов:

  • Закройте затронутые подключения, чтобы клиенты повторно подключались и повторно прошли проверку подлинности. Для проверки подлинности маркера носителя параметр CloseOnAuthenticationExpiration закрывает подключения при истечении срока действия маркера проверки подлинности.
  • Выполняйте проверки авторизации в методах концентратора с текущими данными, например текущими ролями или утверждениями пользователя из хранилища данных, а не полагаться только на кэшированный субъект.

В приложении на основе браузера cookie проверка подлинности позволяет существующим учетным данным пользователя автоматически передаваться на подключения SignalR. При использовании клиента браузера дополнительная конфигурация не требуется. Если пользователь выполнил вход в приложение, это подключение SignalR автоматически использует ту же аутентификацию.

Cookies — это способ отправки маркеров доступа в браузере, но клиенты, не являющиеся пользователями, могут отправлять их. Если используется клиент .NET, свойство Cookies можно настроить в вызове .WithUrl для предоставления cookie. Однако использование cookie аутентификации клиентом .NET требует, чтобы приложение предоставило API для обмена данными аутентификации на cookie.

Это важно

Начиная с ASP.NET Core 10, известные точки доступа API больше не перенаправляются на страницы входа при использовании cookie аутентификации. Вместо этого они возвращают коды состояния 401/403. Дополнительные сведения см. в статье о поведении проверки подлинности конечной точки API в ASP.NET Core.

Bearer Проверка подлинности маркера

Клиент может предоставить маркер доступа вместо использования cookie. Сервер проверяет маркер и использует его для обнаружения пользователя. Для транспортов, выполняющих несколько HTTP-запросов (например, LongPolling и ServerSentEvents), аутентификация выполняется для каждого запроса, но SignalR кэширует полученный principal на время существования подключения. Как и при cookie аутентификации, SignalR не выполняет автоматическую повторную проверку пользователя в течение времени существования подключения, чтобы проверить отзыв маркера или изменения ролей либо утверждений пользователя. Дополнительные сведения см. в разделе "Изменения пользователей и ролей в течение времени существования подключения".

В клиенте JavaScript маркер можно предоставить с помощью параметра accessTokenFactory .

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

В клиенте .NET есть аналогичное свойство AccessTokenProvider , которое можно использовать для настройки маркера:

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

Note

Функция маркера доступа вызывается перед каждым HTTP-запросом, сделанным SignalR. Если токен необходимо обновить, чтобы сохранить подключение активным, выполните это обновление в этой функции и верните обновлённый токен. Возможно, потребуется обновить токен, чтобы срок его действия не истёк во время соединения.

В стандартных веб-API маркеры носителя отправляются в заголовке HTTP. Однако SignalR не может задавать эти заголовки в браузерах при использовании некоторых транспортов. При использовании WebSockets и Server-Sent Events токен передаётся как параметр строки запроса.

Встроенная JWT проверка подлинности

На сервере проверка подлинности маркера носителя настраивается с помощью по промежуточного слоя веб-маркераJWTBearer JSON:

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

Note

Строка запроса используется в браузерах при подключении к WebSockets и событиям server-Sent из-за ограничений API браузера. При использовании HTTPS подключение TLS защищает значения строки запроса. Однако многие серверы фиксируют значения строки запроса в журналах. Дополнительные сведения см. в разделе "Вопросы безопасности" в ASP.NET Core SignalR. SignalR использует заголовки для передачи маркеров в средах, поддерживающих их, таких как .NET и клиенты Java.

IdentityПроверка подлинности сервера JWT

При использовании Duende IdentityServer добавьте PostConfigureOptions<TOptions> службу в проект:

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

Зарегистрируйте службу после добавления служб для проверки подлинности (с AddAuthentication помощью метода) и обработчика проверки подлинности для 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 и маркеры носителя

Cookies относятся к браузерам. Отправка их из других типов клиентов усложняет задачу по сравнению с отправкой токенов носителя. Cookie аутентификация не рекомендуется, если приложению нужно аутентифицировать пользователей только в браузере. Bearer Проверка подлинности маркера — это рекомендуемый подход при использовании клиентов, отличных от клиента браузера.

проверка подлинности Windows

Если аутентификация Windows настроена в приложении, SignalR может использовать это удостоверение для защиты концентраторов. Однако для отправки сообщений отдельным пользователям добавьте настраиваемый поставщик идентификатора пользователя. Система проверка подлинности Windows не предоставляет утверждение "Идентификатор имени". SignalR использует утверждение для определения имени пользователя.

Добавьте новый класс, реализующий IUserIdProvider и извлекающий одно из утверждений пользователя для использования в качестве идентификатора. Например, чтобы использовать утверждение Name (которое является именем пользователя Windows в форме [Domain]/[Username]), создайте следующий класс:

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

Вместо ClaimTypes.Name используйте любое значение из User, например идентификатор идентификатора безопасности Windows и т. д.

Note

Указанное значение должно быть уникальным среди всех пользователей в системе. В противном случае сообщение, предназначенное для одного пользователя, может в конечном итоге достичь другого пользователя.

Зарегистрируйте этот компонент в 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.

В клиенте .NET необходимо включить проверка подлинности Windows, задав свойство UseDefaultCredentials:

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

проверка подлинности Windows поддерживается в Microsoft Edge, но не во всех браузерах. Например, в Chrome и Safari попытка использовать проверку подлинности Windows и WebSockets не удается. Если проверка подлинности Windows завершается неудачей, клиент пытается переключиться на другие транспорты, которые могут сработать.

Использование требований для настройки обработки идентичности

Приложение, которое проверяет подлинность пользователей, может получать SignalR идентификаторы пользователей из утверждений пользователей. Чтобы указать, каким образом SignalR создаёт идентификаторы пользователей, реализуйте IUserIdProvider и зарегистрируйте реализацию.

В примере кода показано, как использовать утверждения для выбора адреса электронной почты пользователя в качестве свойства идентификации.

Note

Указанное значение должно быть уникальным среди всех пользователей в системе. В противном случае сообщение, предназначенное для одного пользователя, может в конечном итоге достичь другого пользователя.

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

Регистрация учетной записи добавляет атрибут типа ClaimsTypes.Email в базу данных идентификаций 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.

Зарегистрируйте этот компонент в Program.cs файле:

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

Авторизовать пользователей для доступа к хабам и методам хаба

По умолчанию пользователь, не прошедший проверку подлинности, может вызывать все методы в концентраторе. Чтобы требовать аутентификацию, примените атрибут AuthorizeAttribute к концентратору.

[Authorize]
public class ChatHub: Hub
{
}

Аргументы конструктора и свойства атрибута [Authorize] можно использовать для ограничения доступа только пользователям, соответствующим определенным политикам авторизации. Например, с помощью настраиваемой политики авторизации, которая называется MyAuthorizationPolicy, только пользователи, соответствующие этой политике, могут получить доступ к центру с помощью следующего кода:

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

Атрибут [Authorize] может применяться к отдельным методам концентратора. Если текущий пользователь не соответствует политике, которая применяется к методу, вызывающей стороне возвращается ошибка.

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

Использование обработчиков авторизации для настройки авторизации метода концентратора

SignalR предоставляет настраиваемый ресурс обработчикам авторизации, если для метода концентратора требуется авторизация. Ресурс является экземпляром HubInvocationContext. Включает HubInvocationContext, имя вызываемого метода концентратора HubCallerContext и аргументы для метода концентратора.

Рассмотрим пример комнаты чата, которая позволяет нескольким организациям входить через Microsoft Entra ID. Любой пользователь с учетной записью Майкрософт может войти в чат, но только члены собственной организации должны иметь возможность запретить пользователям или просматривать журналы чатов пользователей. Кроме того, может потребоваться ограничить некоторые функциональные возможности конкретных пользователей. В этом сценарии обратите внимание на то, как DomainRestrictedRequirement выступает в качестве пользовательского IAuthorizationRequirement. HubInvocationContext Так как параметр ресурса передается, внутренняя логика может проверить контекст, в котором вызывается концентратор, и принять решения о разрешении пользователю выполнять отдельные методы концентратора:

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

Program.cs В файле добавьте новую политику, указав настраиваемое DomainRestrictedRequirement требование в качестве параметра для создания DomainRestricted политики:

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.

В предыдущем примере класс DomainRestrictedRequirement является как IAuthorizationRequirement, так и своим собственным AuthorizationHandler для этого требования. Можно разделить эти два компонента на отдельные классы для разделения проблем. Подход в этом примере дает преимущество не вводить AuthorizationHandler во время запуска, так как требование и обработчик одинаковы.

В качестве альтернативы регистрации DomainRestricted политики и ссылки на нее [Authorize("DomainRestricted")]можно применить IAuthorizationRequirementData атрибут непосредственно к концентратору или методу концентратора. SignalR объединяет требования атрибута в эффективную политику вызова метода. Дополнительные сведения см. в разделе "Пользовательские политики авторизации" с параметром IAuthorizationRequirementData.

Дополнительные ресурсы

Просмотреть или скачать образец кода (описание загрузки)

Проверка подлинности пользователей, подключающихся к концентратору SignalR

SignalR можно использовать с проверкой подлинности ASP.NET Core для ассоциации пользователя с каждым подключением. Данные аутентификации в узле можно получить из свойства HubConnectionContext.User. Аутентификация позволяет концентратору вызывать методы на всех подключениях, связанных с пользователем. Дополнительные сведения см. в разделе "Управление пользователями и группами".SignalR Несколько подключений могут быть связаны с одним пользователем.

Ниже приведен пример Startup.Configure, который использует SignalR и аутентификацию ASP.NET 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?}");
    });
}

Note

Если срок действия маркера истекает во время существования подключения, подключение продолжает работать. LongPolling и ServerSentEvents подключения завершаются сбоем при последующих запросах, если они не отправляют новые маркеры доступа.

В браузерном приложении cookie аутентификация позволяет автоматическому переносу существующих пользовательских учетных данных к подключениям SignalR. При использовании клиента браузера дополнительная конфигурация не требуется. Если пользователь вошел в ваше приложение, SignalR подключение автоматически наследует эту аутентификацию.

Cookies — это способ отправки маркеров доступа в браузере, но клиенты, отличные от браузера, могут отправлять их. При использовании .NET клиента, свойство Cookies можно настроить при вызове .WithUrl для предоставления cookie. Однако использование cookie аутентификации клиентом .NET требует, чтобы приложение предоставило API для обмена данными аутентификации на cookie.

Bearer Проверка подлинности маркера

Клиент может предоставить маркер доступа вместо использования cookie. Сервер проверяет маркер и использует его для обнаружения пользователя. Эта проверка выполняется только при установке подключения. В течение жизни подключения сервер автоматически не перепроверяет отзыв токена.

В клиенте JavaScript маркер можно предоставить с помощью параметра accessTokenFactory .

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

В клиенте .NET есть аналогичное свойство AccessTokenProvider , которое можно использовать для настройки маркера:

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

Note

Функция маркера доступа, которую вы предоставляете, вызывается перед каждым HTTP-запросом, сделанным SignalR. Если необходимо продлить маркер, чтобы сохранить подключение активным (так как оно может истекать во время подключения), сделайте это из этой функции и верните обновленный маркер.

В стандартных веб-API маркеры носителя отправляются в заголовке HTTP. SignalR Однако не удается задать эти заголовки в браузерах при использовании некоторых транспортных средств. При использовании WebSockets и событий, отправленных сервером, маркер передается в качестве параметра строки запроса.

Встроенная JWT проверка подлинности

На сервере проверка подлинности маркера носителя настраивается с помощью по промежуточного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. 
}

Note

Строка запроса используется в браузерах при подключении к WebSockets и событиям server-Sent из-за ограничений API браузера. При использовании HTTPS значения строк запроса защищены подключением TLS. Однако многие серверы фиксируют значения строки запроса в журналах. Дополнительные сведения см. в разделе "Вопросы безопасности" в ASP.NET Core SignalR. SignalR использует заголовки для передачи маркеров в средах, которые поддерживают их (например, клиенты .NET и Java).

IdentityПроверка подлинности сервера JWT

При использовании Identity сервера добавьте PostConfigureOptions<TOptions> службу в проект:

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

Зарегистрируйте службу в Startup.ConfigureServices после добавления сервисов для аутентификации (AddAuthentication) и обработчика аутентификации для сервера Identity (AddIdentityServerJwt):

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

Cookies vs. маркеры носителя

Cookies относятся к браузерам. Отправка их из других типов клиентов усложняет задачу по сравнению с отправкой токенов носителя. Следовательно, проверка подлинности не рекомендуется, cookie если приложение не должно проходить проверку подлинности пользователей из клиента браузера. Bearer Проверка подлинности маркера — это рекомендуемый подход при использовании клиентов, отличных от клиента браузера.

проверка подлинности Windows

Если проверка подлинности Windows настроена в приложении, SignalR может использовать это удостоверение для защиты центров. Однако для отправки сообщений отдельным пользователям необходимо добавить настраиваемый поставщик идентификаторов пользователей. Система проверка подлинности Windows не предоставляет утверждение "Идентификатор имени". SignalR использует утверждение для определения имени пользователя.

Добавьте новый класс, реализующий IUserIdProvider, и извлеките одно из утверждений пользователя, чтобы использовать его в качестве идентификатора. Например, чтобы использовать утверждение Name (которое является именем пользователя Windows в форме [Domain]\[Username]), создайте следующий класс:

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

Вместо ClaimTypes.Name можно использовать любое значение из User (например, идентификатор безопасности Windows и т. д.).

Note

Выбранное значение должно быть уникальным среди всех пользователей в вашей системе. В противном случае сообщение, предназначенное для одного пользователя, может в конечном итоге перейти к другому пользователю.

Зарегистрируйте этот компонент в Startup.ConfigureServices методе.

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

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

В клиенте .NET необходимо включить аутентификацию Windows, задав свойство UseDefaultCredentials.

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

проверка подлинности Windows поддерживается в Internet Explorer и Microsoft Edge, но не во всех браузерах. Например, в Chrome и Safari попытка использовать проверку подлинности Windows и WebSockets не удается. Когда проверка подлинности Windows не удается, клиент пытается возвратиться на другие транспорты, которые могут работать.

Использование требований для настройки обработки идентичности

Приложение, которое проверяет подлинность пользователей, может получать SignalR идентификаторы пользователей из утверждений пользователей. Чтобы указать, каким образом SignalR создаёт идентификаторы пользователей, реализуйте IUserIdProvider и зарегистрируйте реализацию.

В примере кода показано, как использовать утверждения для выбора адреса электронной почты пользователя в качестве свойства идентификации.

Note

Выбранное значение должно быть уникальным среди всех пользователей в вашей системе. В противном случае сообщение, предназначенное для одного пользователя, может в конечном итоге перейти к другому пользователю.

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

Регистрация учетной записи добавляет атрибут типа ClaimsTypes.Email в базу данных идентификаций 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));

Зарегистрируйте этот компонент в вашей учетной записи Startup.ConfigureServices.

services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

Авторизовать пользователей для доступа к хабам и методам хаба

По умолчанию все методы в концентраторе могут вызываться пользователем без проверки подлинности. Чтобы требовать аутентификацию, примените атрибут AuthorizeAttribute к концентратору.

[Authorize]
public class ChatHub: Hub
{
}

Можно использовать аргументы конструктора и свойства атрибута [Authorize], чтобы ограничить доступ только пользователями, соответствующими определенным политикам авторизации. Например, если у вас есть настраиваемая политика MyAuthorizationPolicy авторизации, можно убедиться, что только пользователи, соответствующие этой политике, могут получить доступ к центру с помощью следующего кода:

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

Отдельным методам концентратора также может быть присвоен атрибут [Authorize]. Если текущий пользователь не соответствует политике, которая применяется к методу, вызывающей стороне возвращается ошибка.

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

Использование обработчиков авторизации для настройки авторизации метода концентратора

SignalR предоставляет настраиваемый ресурс обработчикам авторизации, если для метода концентратора требуется авторизация. Ресурс является экземпляром HubInvocationContext. Включает HubInvocationContext, имя вызываемого метода концентратора HubCallerContext и аргументы для метода концентратора.

Рассмотрим пример комнаты чата, разрешающей вход в несколько организаций с помощью идентификатора Microsoft Entra. Любой пользователь с учетной записью Майкрософт может войти в чат, но только члены собственной организации должны иметь возможность запретить пользователям или просматривать журналы чатов пользователей. Кроме того, может потребоваться ограничить определенные функциональные возможности от определенных пользователей. Использование обновленных функций в ASP.NET Core 3.0 вполне возможно. Обратите внимание, как DomainRestrictedRequirement служит в качестве пользовательского IAuthorizationRequirement. Теперь, когда HubInvocationContext параметр ресурса передается, внутренняя логика может проверить контекст, в котором вызывается Концентратор, и принимать решения о разрешении пользователю выполнять отдельные методы Концентратора.

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

Добавьте Startup.ConfigureServicesновую политику, указав настраиваемое DomainRestrictedRequirement требование в качестве параметра для создания DomainRestricted политики.

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

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

В предыдущем примере класс DomainRestrictedRequirement является как IAuthorizationRequirement, так и своим собственным AuthorizationHandler для этого требования. Можно разделить эти два компонента на отдельные классы для разделения проблем. Преимуществом подхода в примере является отсутствие необходимости внедрения AuthorizationHandler во время запуска, поскольку требование и обработчик — это одно и то же.

Дополнительные ресурсы