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

注意

如果令牌在连接生存期内过期,默认情况下连接会继续工作。 如果后续请求不发送新的访问令牌,LongPollingServerSentEvent 连接将失败。 若要在身份验证令牌过期时关闭连接,请设置 CloseOnAuthenticationExpiration

在基于浏览器的应用中,cookie 身份验证允许现有用户凭据自动流向 SignalR 连接。 使用浏览器客户端时,无需进行额外配置。 如果用户已登录到应用,SignalR 连接会自动继承此身份验证。

Cookie 是一种特定于浏览器的访问令牌发送方式,但非浏览器客户端也可以发送访问令牌。 使用 .NET 客户端时,可以在 .WithUrl 调用中配置 Cookies 属性以提供 cookie。 但是,从 .NET 客户端使用 cookie 身份验证时,应用必须提供 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();

注意

提供的访问令牌函数在 SignalR 发出的每个 HTTP 请求之前调用。 如果需要续订令牌使连接保持活动状态,请在此函数中执行此操作并返回更新的令牌。 可能需要续订令牌,以便在连接期间不会过期。

在标准 Web API 中,持有者令牌在 HTTP 标头中发送。 但是,当使用某些传输时,SignalR 无法在浏览器中设置这些标头。 使用 WebSocket 和服务器发送的事件时,令牌作为查询字符串参数传输。

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

注意

由于存在浏览器 API 限制,当连接 WebSocket 和服务器发送的事件时,将在浏览器上使用查询字符串。 使用 HTTPS 时,查询字符串值受 TLS 连接保护。 但是,许多服务器会记录查询字符串值。 有关详细信息,请参阅 ASP.NET Core SignalR 中的安全注意事项。 SignalR 使用标头在支持令牌的环境(例如 .NET 和 Java 客户端)中传输令牌。

Identity Server JWT 身份验证

使用 Duende Identity Server 时,将 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 Server 的身份验证处理程序 (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 与持有者令牌

Cookie 特定于浏览器。 与发送持有者令牌相比,从其他类型的客户端发送 cookie 会增加复杂性。 除非应用只需从浏览器客户端对用户进行身份验证,否则不建议使用 Cookie 身份验证。 当使用浏览器客户端以外的客户端时,建议使用持有者令牌身份验证方法。

Windows 身份验证

如果在应用中配置了 Windows 身份验证,SignalR 可以使用该标识来保护中心。 但是,若要向单个用户发送消息,需要添加自定义用户 ID 提供程序。 Windows 身份验证系统不提供“名称标识符”声明。 SignalR 使用该声明来确定用户名。

添加一个实现 IUserIdProvider 的新类,并从用户那里检索一个声明以用作标识符。 例如,若要使用“名称”声明(即 [Domain]/[Username] 形式的 Windows 用户名),请创建以下类:

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

除了 ClaimTypes.Name,可以使用 User 中的任何值(例如 Windows SID 标识符等)。

注意

所选值对于系统中的所有用户必须是唯一的。 否则,发送给某个用户的消息可能最终会发送给另一个用户。

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 客户端中,必须通过设置 UseDefaultCredentials 属性来启用 Windows 身份验证:

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

Microsoft Edge 支持 Windows 身份验证,但并非所有浏览器都支持。 例如,在 Chrome 和 Safari 中,尝试使用 Windows 身份验证和 WebSocket 会失败。 当 Windows 身份验证失败时,客户端会尝试回退到其他可能有效的传输方式。

使用声明自定义标识处理

对用户进行身份验证的应用可以从用户声明中派生 SignalR 用户 ID。 若要指定 SignalR 如何创建用户 ID,请实现 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 ID 进行多次组织登录的聊天室示例。 拥有 Microsoft 帐户的任何人都可以登录聊天,但只有所属组织的成员才能阻止用户或查看用户的聊天历史记录。 而且,我们可能希望禁止特定用户使用某些功能。 注意 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 中管理用户和组。 单个用户可以关联多个连接。

以下是使用 SignalR 和 ASP.NET Core 身份验证的 Startup.Configure 的示例:

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?}");
    });
}

注意

如果令牌在连接生存期内过期,连接将继续工作。 如果后续请求不发送新的访问令牌,LongPollingServerSentEvent 连接将失败。

在基于浏览器的应用中,cookie 身份验证允许现有用户凭据自动流向 SignalR 连接。 使用浏览器客户端时,无需进行其他配置。 如果用户已登录到你的应用,SignalR 连接会自动继承此身份验证。

Cookie 是一种特定于浏览器的访问令牌发送方式,但非浏览器客户端也可以发送访问令牌。 使用 .NET 客户端时,可以在 .WithUrl 调用中配置 Cookies 属性以提供 cookie。 但是,从 .NET 客户端使用 cookie 身份验证时,应用必须提供 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();

注意

你提供的访问令牌函数在 SignalR 发出的每个 HTTP 请求之前调用。 如果需要续订令牌,以使连接保持活动状态(因为它可能在连接期间过期),请在此函数中执行此操作并返回更新后的令牌。

在标准 Web API 中,持有者令牌在 HTTP 标头中发送。 但是,当使用某些传输时,SignalR 无法在浏览器中设置这些标头。 使用 WebSocket 和服务器发送的事件时,令牌作为查询字符串参数传输。

内置 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 讨论问题中告诉我们。

注意

由于存在浏览器 API 限制,当连接 WebSocket 和服务器发送的事件时,将在浏览器上使用查询字符串。 使用 HTTPS 时,查询字符串值受 TLS 连接保护。 但是,许多服务器会记录查询字符串值。 有关详细信息,请参阅 ASP.NET Core SignalR 中的安全注意事项。 SignalR 使用标头在支持令牌的环境(例如 .NET 和 Java 客户端)中传输令牌。

Identity Server JWT 身份验证

使用 Identity Server 时,将 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 Server 的身份验证处理程序 (AddIdentityServerJwt) 后,在 Startup.ConfigureServices 中注册服务:

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

Cookies 与持有者令牌

Cookie 特定于浏览器。 与发送持有者令牌相比,从其他类型的客户端发送 cookie 会增加复杂性。 因此,除非应用只需从浏览器客户端对用户进行身份验证,否则不建议使用 cookie 身份验证。 当使用浏览器客户端以外的客户端时,建议使用持有者令牌身份验证方法。

Windows 身份验证

如果在应用中配置了 Windows 身份验证,SignalR 可以使用该标识来保护中心。 但是,若要向单个用户发送消息,你需要添加自定义用户 ID 提供程序。 Windows 身份验证系统不提供“名称标识符”声明。 SignalR 使用该声明来确定用户名。

添加一个实现 IUserIdProvider 的新类,并从用户那里检索一个声明以用作标识符。 例如,若要使用“名称”声明(即 [Domain]\[Username] 形式的 Windows 用户名),请创建以下类:

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

除了 ClaimTypes.Name,你可以使用 User 中的任何值(例如 Windows SID 标识符等)。

注意

所选值对于系统中的所有用户必须是唯一的。 否则,发送给某个用户的消息可能最终会发送给另一个用户。

Startup.ConfigureServices 方法中注册此组件。

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

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

在 .NET 客户端中,必须通过设置 UseDefaultCredentials 属性来启用 Windows 身份验证:

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

Internet Explorer 和 Microsoft Edge 支持 Windows 身份验证,但并非所有浏览器都支持。 例如,在 Chrome 和 Safari 中,尝试使用 Windows 身份验证和 WebSocket 会失败。 当 Windows 身份验证失败时,客户端会尝试回退到其他可能有效的传输方式。

使用声明自定义标识处理

对用户进行身份验证的应用可以从用户声明中派生 SignalR 用户 ID。 若要指定 SignalR 如何创建用户 ID,请实现 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 ID 进行多次组织登录的聊天室示例。 拥有 Microsoft 帐户的任何人都可以登录聊天,但只有所属组织的成员才能阻止用户或查看用户的聊天历史记录。 而且,我们可能希望禁止某些用户使用某些功能。 借助 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,因为要求和处理程序是同一个类。

其他资源