Проверка подлинности и авторизация в 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();

Примечание.

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

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

Cookies — это способ отправки маркеров доступа в браузере, но клиенты, отличные от браузера, могут отправлять их. При использовании клиентаCookies .NET свойство можно настроить в вызове .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();

Примечание.

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

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

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

На сервере проверка подлинности маркера носителя настраивается с помощью ПО промежуточного слоя носителя JWT:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddAuthentication(options =>
{
    // Identity made Cookie authentication the default.
    // However, we want JWT Bearer Auth to be the default.
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
  {
      // Configure the Authority to the expected value for
      // the authentication provider. This ensures the token
      // is appropriately validated.
      options.Authority = "Authority URL"; // TODO: Update URL

      // We have to hook the OnMessageReceived event in order to
      // allow the JWT authentication handler to read the access
      // token from the query string when a WebSocket or 
      // Server-Sent Events request comes in.

      // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
      // due to a limitation in Browser APIs. We restrict it to only calls to the
      // SignalR hub in this code.
      // See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
      // for more information about security considerations when using
      // the query string to transmit the access token.
      options.Events = new JwtBearerEvents
      {
          OnMessageReceived = context =>
          {
              var accessToken = context.Request.Query["access_token"];

              // If the request is for our hub...
              var path = context.HttpContext.Request.Path;
              if (!string.IsNullOrEmpty(accessToken) &&
                  (path.StartsWithSegments("/hubs/chat")))
              {
                  // Read the token out of the query string
                  context.Token = accessToken;
              }
              return Task.CompletedTask;
          }
      };
  });

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token 
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages 
// intended for a different user!
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

// Change to use email as the user identifier for SignalR
// builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

// WARNING: use *either* the NameUserIdProvider *or* the 
// EmailBasedUserIdProvider, but do not use both. 

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

Примечание.

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

Проверка подлинности 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 и т. д.

Примечание.

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

Регистрация этого компонента в 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 и зарегистрируйте реализацию.

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

Примечание.

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

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.

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

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

Просмотрите или скачайте пример кода (описание процедуры скачивания)

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

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

Ниже приведен пример Startup.Configure использования и проверки SignalR подлинности core 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?}");
    });
}

Примечание.

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

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

Cookies — это способ отправки маркеров доступа в браузере, но клиенты, отличные от браузера, могут отправлять их. При использовании клиентаCookies .NET свойство можно настроить в вызове .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();

Примечание.

Предоставляемая функция маркера доступа вызывается перед каждым 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. 
}

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

Примечание.

Строка запроса используется в браузерах при подключении к 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 и маркеры носителя

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

Проверка подлинности 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 и т. д.).

Примечание.

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

Зарегистрируйте этот компонент в 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 поддерживается в Интернете Обозреватель и Microsoft Edge, но не во всех браузерах. Например, в Chrome и Safari попытка использовать проверка подлинности Windows и WebSockets завершается ошибкой. При сбое проверка подлинности Windows клиент пытается вернуться к другим транспортам, которые могут работать.

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

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

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

Примечание.

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

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

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

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