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

Эндрю Стантон-медперсонала

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

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

SignalR можно использовать с проверкой подлинности ASP.NET Core , чтобы связать пользователя с каждым соединением. В центре данные проверки подлинности можно получить из свойства хубконнектионконтекст. 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?}");
    });
}
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseAuthentication();

    app.UseSignalR(hubs =>
    {
        hubs.MapHub<ChatHub>("/chat");
    });

    app.UseMvc(routes =>
    {
        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}

Примечание

Порядок, в котором регистрируется SignalR и ASP.NET Core по промежуточного слоя проверки подлинности имеет значение. Всегда вызывайте метод UseAuthentication перед UseSignalR , чтобы SignalR иметь пользователя в HttpContext .

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

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

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

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

В клиенте JavaScript маркер можно указать с помощью параметра акцесстокенфактори .

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

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

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

Примечание

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

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

Встроенная проверка подлинности 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 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. Однако многие серверы заносить в журнал значения строки запроса. Дополнительные сведения см. в разделе вопросы безопасности в SignalR ASP.NET Core . 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

Если в приложении настроена Проверка подлинности 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 (например, идентификатор SID Windows и т. д.).

Примечание

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

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

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

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

В клиенте .NET проверка подлинности Windows должна быть включена путем установки свойства уседефаулткредентиалс :

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

Проверка подлинности Windows поддерживается в Internet Explorer и Microsoft погранично, но не во всех браузерах. Например, в 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>();

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

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

[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 , имя вызываемого метода концентратора и аргументы для метода концентратора.

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

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