当客户端使用SignalR Client 连接到服务端的 SignalR Server 后会生成一个连接Id,这个连接Id 我们可以通过 Context.ConnectionId
来获取。它存在一个问题,客户端和服务端的 SignalR 连接意外断开重新连接后 Context.ConnectionId
会发生变化,这看起来没什么,对于单体应用来说在任何情况下重新连接还是连接的同一个服务,但是对于微服务系统来说连接一旦意外断开,再次重连就有很大的可能连接到别的同类型服务上。虽然 websocket 服务是无状态的,但是在某些情况下我们还是要在websocket中实现有状态的操作(例如通过websocket获取某个服务器正在计算的数值)。 要解决这个问题有两种方法:一种是比较复杂的,将websocket中的有状态操作全部改为无状态的,另一种比较简单,使用自定义的连接ID。第一种方法涉及业务代码的修改,在这里我们不讲,我们只讲解如何自定义连接ID。
一、配置
- 注入
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.DefaultAuthenticateScheme
和 options.DefaultChallengeScheme
两个属性分别指定了默认的身份验证方案和质询方案为JwtBearerDefaults.AuthenticationScheme
,即使用JWT Bearer认证方式。
AddJwtBearer
用于配置JWT Bearer身份验证。在 options.TokenValidationParameters
中我们定义了多个令牌验证的参数
ValidateIssuer
:是否验证令牌的颁发者;ValidateAudience
:是否验证令牌的受众;ValidateLifetime
:是否验证令牌的有效期;ValidateIssuerSigningKey
:是否验证签名密钥;ValidIssuer
和ValidAudience
:从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 改变的问题。
以下有两点需要注意:
- 自定义连接Id必须唯一
- 自定义连接Id必须存放在Token中
- 使用
IHubContext.Clients.User(id).SendAsync
方法来实现向客户端发送消息