Проверка подлинности и авторизация в 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 закрывает подключения при истечении срока действия маркера проверки подлинности.
  • Выполняйте проверки авторизации в методах концентратора с текущими данными, например текущими ролями или утверждениями пользователя из хранилища данных, а не полагаться только на кэшированный субъект.

В .NET 11 и более поздних версий клиент может обновить учетные данные для активного подключения без повторного подключения. Когда клиент предоставляет обновлённый токен, сервер повторно аутентифицирует его и заменяет кэшированный Context.User непосредственно в кэше, поэтому последующие вызовы методов хаба проходят авторизацию с учётом обновлённых ролей и утверждений. Обновленный субъект должен сопоставляться с тем же SignalR пользователем, поэтому роли и утверждения обновления обновляются, но не изменяют удостоверение пользователя или маршрутизацию подключения. Дополнительные сведения см. в разделе "Обновление проверки подлинности".

В приложении на основе браузера 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 аутентификация

На сервере аутентификация с помощью токена носителя настраивается с использованием промежуточного слоя JSON web token (JWT) Bearer:

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.

Обновление аутентификации

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

Обновление проверки подлинности, доступное в .NET 11 и более поздних версий, позволяет клиенту обновлять учетные данные для активного подключения без повторного подключения. Сервер повторно проходит проверку подлинности запроса на обновление через обычный конвейер авторизации конечной точки и заменяет подключение ClaimsPrincipal , если обновленный субъект сопоставляется с тем же SignalR пользователем.

Включение обновления проверки подлинности на сервере

Включите обновление аутентификации в параметрах концентратора MapHub, установив для EnableAuthenticationRefresh значение true. Включите это вместе с CloseOnAuthenticationExpiration, чтобы подключение, токен которого истекает без своевременного обновления, закрывалось, а не оставалось открытым с устаревшими учётными данными:

app.MapHub<ChatHub>("/chat", options =>
{
    options.CloseOnAuthenticationExpiration = true;
    options.EnableAuthenticationRefresh = true;
});

Если обновление проверки подлинности включено и билет проверки подлинности имеет срок действия, ответ Negotiate содержит сведения об оставшемся сроке действия маркера, чтобы клиент мог планировать обновления.

Чтобы проверить или отклонить обновление, задайте колбэк OnAuthenticationRefresh. Он выполняется после проверки подлинности запроса на обновление, но до замены пользователя подключения. Верните false, чтобы отклонить обновление; в этом случае конечная точка возвращает код состояния HTTP 403, а соединение сохраняет текущего пользователя. Обратный вызов — это дополнительная проверка на основе встроенной проверки того, что обновленный субъект сопоставляется с тем же SignalR пользователем. Можно отклонить обновление, но нельзя утвердить обновление, которое не проходит встроенную проверку:

app.MapHub<ChatHub>("/chat", options =>
{
    options.CloseOnAuthenticationExpiration = true;
    options.EnableAuthenticationRefresh = true;
    options.OnAuthenticationRefresh = context =>
    {
        if (!context.NewUser.HasClaim("tenant", "contoso"))
        {
            return Task.FromResult(false);
        }

        return Task.FromResult(true);
    };
});

Обновленный субъект должен сопоставляться с тем же SignalR пользователем, что и подключение. Если он сопоставляется с другим идентификатором пользователя, обновление отклоняется: конечная точка отвечает с кодом состояния HTTP 403, а подключение сохраняет текущего пользователя и остается подключенным. Обновление никогда не изменяет Context.UserIdentifier подключения и не перенаправляет сообщения, отправленные с помощью Clients.User, даже при успешном обновлении. Идентификатор маршрутизации задаётся при установлении соединения. Чтобы изменить его, повторно подключите клиент.

Чтобы определить, насколько далеко обновление может расширить срок действия проверки подлинности подключения, задайте MaximumAuthenticationExpiration. Обновлённый срок действия ограничен не более чем этим промежутком времени с текущего момента, даже если токен указывает более длительный срок действия. Это ограничение применяется всегда, когда включено обновление аутентификации, в том числе когда у токена не задан собственный срок действия. В этом случае лимит задаёт для подключения известный срок действия, поэтому в ответе negotiate указывается срок действия токена, и клиент может настроить его автоматическое обновление. Значение должно быть больше нуля и не применяется к проверка подлинности Windows, которая никогда не отслеживается или не обновляется.

Обновление аутентификации в клиенте .NET

Клиент .NET обновляет учетные данные, используя AccessTokenProvider, настроенный для подключения. Каждое обновление вызывает AccessTokenProvider, чтобы получить новый токен доступа, а не использовать повторно токен, кэшированный при установлении соединения.

Чтобы выполнить явное обновление, вызовите RefreshAuthenticationAsync, который возвращает срок действия нового токена, о котором сообщает сервер:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chat", options =>
    {
        options.AccessTokenProvider = GetAccessTokenAsync;
    })
    .Build();

await connection.StartAsync();

TimeSpan? newLifetime = await connection.RefreshAuthenticationAsync();

Чтобы автоматически обновлять токен перед истечением срока его действия, вызовите WithAuthenticationRefresh и настройте AuthenticationRefreshOptions:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chat", options =>
    {
        options.AccessTokenProvider = GetAccessTokenAsync;
    })
    .WithAuthenticationRefresh(options =>
    {
        options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2);
    })
    .Build();

AuthenticationRefreshOptions предоставляет следующие параметры:

  • EnableAutoRefresh: включает автоматическое обновление до истечения срока действия маркера. По умолчанию — true. Клиент планирует обновление только тогда, когда сервер сообщает срок действия токена. Если сервер не сообщает о времени существования, автоматическое обновление не запланировано и RefreshAuthenticationAsync по-прежнему может вызываться вручную.
  • RefreshBeforeExpiration: За какое время до указанного срока действия следует выполнять обновление. Значение по умолчанию — пять минут.

Чтобы отслеживать обновления, обрабатывайте события AuthenticationRefreshed и AuthenticationRefreshFailed подключения. Автоматическое и ручное обновление вызывают следующие события:

connection.AuthenticationRefreshed += context =>
{
    Console.WriteLine(
        $"Authentication refreshed. New lifetime: {context.NewTokenLifetime}");
    return Task.CompletedTask;
};

connection.AuthenticationRefreshFailed += context =>
{
    Console.WriteLine(
        $"Authentication refresh failed: {context.Exception}");
    return Task.CompletedTask;
};

Обновление аутентификации в клиенте JavaScript

Клиент JavaScript обновляет учетные данные, используя accessTokenFactory, настроенный для подключения. При каждом обновлении вызывается accessTokenFactory для получения нового маркера доступа.

Чтобы принудительно обновить, вызовите refreshAuthentication, который возвращает новое время жизни токена в секундах, сообщаемое сервером:

const newLifetimeInSeconds = await connection.refreshAuthentication();

Чтобы автоматически обновлять токен до истечения срока его действия, вызовите withAuthenticationRefresh и при необходимости настройте IAuthenticationRefreshOptions. Обработка результатов обновления с помощью onAuthenticationRefreshed и onAuthenticationRefreshFailed:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chat", {
        accessTokenFactory: () => getAccessToken()
    })
    .withAuthenticationRefresh({
        refreshBeforeExpirationInMilliseconds: 120000
    })
    .build();

connection.onAuthenticationRefreshed(context => {
    console.log(
        `Authentication refreshed. New lifetime: ${context.newTokenLifetimeInSeconds}`);
});

connection.onAuthenticationRefreshFailed(context => {
    console.log(`Authentication refresh failed: ${context.error}`);
});

IAuthenticationRefreshOptions предоставляет следующие параметры:

  • enableAutoRefresh: включает автоматическое обновление до истечения срока действия маркера. По умолчанию — true. Как и в случае с клиентом .NET, автоматическое обновление планируется только в том случае, если сервер сообщает время существования маркера.
  • refreshBeforeExpirationInMilliseconds: За сколько миллисекунд до сообщаемого истечения срока действия выполнять обновление. По умолчанию используется значение 300 000 (пять минут).

Реагирование на обновление в центре

Переопределите OnAuthenticationRefreshedAsync в Hub, чтобы выполнять код после того, как к подключению будут применены обновлённые учётные данные пользователя. Context.User отражает обновленный субъект. Как описано ранее, Context.UserIdentifier и SignalR маршрутизация пользователей не изменяется при обновлении:

public class ChatHub : Hub
{
    public override Task OnAuthenticationRefreshedAsync()
    {
        return Clients.Caller.SendAsync(
            "AuthenticationRefreshed", Context.UserIdentifier);
    }
}

Метод концентратора, который уже выполняется, сохраняет Context.User, с которым он был запущен. При последующих вызовах используется обновлённый Context.User. Дополнительные сведения о том, как SignalR кэширует пользователя, прошедшего проверку подлинности, см. в разделе Изменения пользователей и ролей в течение времени жизни подключения.

Для обновления аутентификации требуется подключение, в котором согласована версия протокола 1 или выше. Автоматическое обновление выполняется по расписанию только в том случае, если сервер сообщает срок действия токена, который обычно предоставляется схемой аутентификации, задающей время истечения, например токенами Bearer. проверка подлинности Windows не сообщает об истечении срока действия и не отслеживается или обновляется этой функцией.

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 bearer middleware:

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 во время запуска, поскольку требование и обработчик — это одно и то же.

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