身份验证和授权 ASP.NET CoreSignalR

查看或下载示例代码(如何下载)

对连接到中心 SignalR 的用户进行身份验证

SignalR可以与身份验证ASP.NET Core,以将用户与每个连接关联。 在中心,可以从 HubConnectionContext.User 属性访问身份验证数据。 身份验证允许中心对与用户关联的所有连接调用方法。 有关详细信息,请参阅在 中管理用户和 SignalR 组。 多个连接可以与单个用户关联。

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

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

备注

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

备注

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

在基于浏览器的应用中 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();

备注

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

在标准 Web API 中,在 HTTP 标头中发送包含令牌。 但是, SignalR 在使用某些传输时无法在浏览器中设置这些标头。 使用 WebSockets 和Server-Sent事件时,令牌作为查询字符串参数传输。

内置 JWT 身份验证

在服务器上,使用 JWT Bearer 中间件配置 了 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 讨论问题中告诉我们。

备注

由于浏览器 API 限制,使用 WebSockets 进行连接Server-Sent在浏览器中使用查询字符串。 使用 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 Server Identity () : AddIdentityServerJwt

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

Cookies 与 bearer 令牌

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

Windows 身份验证

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

添加一个新类,该类实现并检索用户中的一个声明 IUserIdProvider ,以用作标识符。 例如,若要使用"Name"声明 (,Windows格式的用户名) , [Domain]\[Username] 请创建以下类:

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

可以使用字符串中的任意值( (SID 标识符Windows等等)来 ClaimTypes.Name User) 。

备注

你选择的值在系统中所有用户中必须是唯一的。 否则,针对一个用户的消息最终可能会发送到其他用户。

在 方法中注册此 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 用户声明派生用户 ID。 若要指定 SignalR 如何创建用户 ID,请实现 IUserIdProvider 并注册实现。

示例代码演示如何使用声明选择用户的电子邮件地址作为标识属性。

备注

你选择的值在系统中所有用户中必须是唯一的。 否则,针对一个用户的消息最终可能会发送到其他用户。

public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}

帐户注册会将类型为 ASP.NET ClaimsTypes.Email 声明。

// 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 属性:

[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。 任何拥有Microsoft 帐户的用户都可以登录聊天,但只有拥有组织的成员才能禁止用户或查看用户的聊天历史记录。 此外,我们可能需要限制某些用户的某些功能。 使用 ASP.NET Core 3.0 中的更新功能,这完全可行。 请注意 如何 DomainRestrictedRequirement 充当自定义 IAuthorizationRequirement 。 传入资源参数后,内部逻辑可以检查调用中心的上下文,并做出允许用户执行各个 HubInvocationContext Hub 方法的决策。

[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 是相同的。

其他资源