Проверка подлинности и авторизация в 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 и ServerSentEvent подключения завершаются сбоем при последующих запросах, если они не отправляют новые маркеры доступа. Чтобы закрыть подключения при истечении срока действия токена аутентификации, установите в настройках CloseOnAuthenticationExpiration.

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

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

Это важно

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

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

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

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.

Файлы cookie и токены носителя

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

Windows authentication

Если аутентификация 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 (SID) и т. д.

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. Любой пользователь с учетной записью Майкрософт может войти в чат, но только члены собственной организации должны иметь возможность запретить пользователям или просматривать журналы чатов пользователей. Кроме того, может потребоваться ограничить некоторые функциональные возможности конкретных пользователей. Обратите внимание, как 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 во время запуска, поскольку требование и обработчик — это одно и то же.

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

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

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

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

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

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

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

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

Файлы cookie и токены носителя

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

Windows authentication

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

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