SignalR 自定义连接ID--基于Token实现

当客户端使用SignalR Client 连接到服务端的 SignalR Server 后会生成一个连接Id,这个连接Id 我们可以通过 Context.ConnectionId 来获取。它存在一个问题,客户端和服务端的 SignalR 连接意外断开重新连接后 Context.ConnectionId 会发生变化,这看起来没什么,对于单体应用来说在任何情况下重新连接还是连接的同一个服务,但是对于微服务系统来说连接一旦意外断开,再次重连就有很大的可能连接到别的同类型服务上。虽然 websocket 服务是无状态的,但是在某些情况下我们还是要在websocket中实现有状态的操作(例如通过websocket获取某个服务器正在计算的数值)。 要解决这个问题有两种方法:一种是比较复杂的,将websocket中的有状态操作全部改为无状态的,另一种比较简单,使用自定义的连接ID。第一种方法涉及业务代码的修改,在这里我们不讲,我们只讲解如何自定义连接ID。

一、配置

  1. 注入 Authentication 服务以及JwtBearer
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = configuration["Token:Issuer"],
        ValidAudience = configuration["Token:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Token:SecretKey"]))
    };
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = (context) =>
        {
            if (!context.HttpContext.Request.Path.HasValue)
            {
                return Task.CompletedTask;
            }
            var accessToken = context.Request.Query["access_token"];
            var path = context.HttpContext.Request.Path;
            if (!(string.IsNullOrWhiteSpace(accessToken)) && path.StartsWithSegments("/GatewayHub"))
            {
                if (!context.Request.Headers.ContainsKey("Authorization"))
                    context.Request.Headers.Add("Authorization", "Bearer " + accessToken);
                return Task.CompletedTask;
            }
            return Task.CompletedTask;
        }
    };
});

在上面的代码中,AddAuthentication 用于向 Asp.net Core的依赖注入容器中添加身份验证服务。options.DefaultAuthenticateSchemeoptions.DefaultChallengeScheme 两个属性分别指定了默认的身份验证方案和质询方案为JwtBearerDefaults.AuthenticationScheme,即使用JWT Bearer认证方式。
AddJwtBearer 用于配置JWT Bearer身份验证。在 options.TokenValidationParameters 中我们定义了多个令牌验证的参数

  • ValidateIssuer:是否验证令牌的颁发者;
  • ValidateAudience:是否验证令牌的受众;
  • ValidateLifetime:是否验证令牌的有效期;
  • ValidateIssuerSigningKey:是否验证签名密钥;
  • ValidIssuerValidAudience:从configuration中读取配置的令牌颁发者和令牌受众;
  • IssuerSigningKey:使用对称安全密钥 SymmetricSecurityKey 进行签名验证.

JwtBearerEvents 用于处理JWT身份验证过程中的事件,其中OnMessageReceived事件是在接收到消息时触发。首先它检查请求的路径是否存在,然后从查询字符串中获取access_token,这个 access_token 是SignalR 请求验证权限和跨域的时候URL中携带的。如果access_token不为空且请求路径以/GatewayHub(我们自己的SignalR服务地址)开头,则将access_token添加到请求头中,格式为Authorization: Bearer <token>

二、实现

实现的方式有两种,一种是通过 SignalR Hub 连接上下文获取,另一种是直接读取请求头中的Authorization 来获取。
2.1 实现1
我们定义了一个UserIdProvider类,它实现了IUserIdProvider接口,用于在 SignalR中提供用户标识User ID。SignalR使用IUserIdProvider接口来确定每个连接的用户ID,并将它用于分组和广播消息等场景。

public class UserIdProvider : IUserIdProvider
{
    /// <summary>
    /// 获取用户id
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    public string? GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.PrimarySid)?.Value;
    }
}

在代码中,GetUserId 方法用于从给定的 HubConnectionContext 对象中获取用户的ID。HubConnectionContext connection 是 SignalR Hub 的连接上下文。它包含关于当前连接的各种信息。

Tip: 这种实现方式我们必须在项目中注入身份验证服务,而且必须指定默认的身份验证方案和默认的质询方案。

2.2 实现2
这种实现方法很简单,直接读取请求头中的Authorization 并解析Token 拿到存储在其中的自定义ID。

public class UserIdProvider : IUserIdProvider
{
    /// <summary>
    /// 获取用户id
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    public string? GetUserId(HubConnectionContext connection)
    {
        var token = connection.GetHttpContext().Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
        if (string.IsNullOrEmpty(token))
            return "";
        var handler = new JwtSecurityTokenHandler();
        var jwtToken = handler.ReadToken(token) as JwtSecurityToken;
        var primarySidClaim = jwtToken?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.PrimarySid)?.Value;
        return primarySidClaim;
    }
}

在代码中,我们读取了token,并使用.NET Core 内置的token解析方法 ReadToken 读取token中的全部内容,最后从Claims 中读取到自定义的ID。

三、总结

本文主要讲解了如何通过自定义SignalR 连接Id来实现长连接意外断开后,重新连接连接Id 改变的问题。
以下有两点需要注意:

  1. 自定义连接Id必须唯一
  2. 自定义连接Id必须存放在Token中
  3. 使用IHubContext.Clients.User(id).SendAsync方法来实现向客户端发送消息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喵叔哟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值