You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SignalR使用WebSocket无法获取HubCallerContext用户、长轮询断连无法感知怎么办

问题根因说明

  1. WebSocket模式下拿不到用户ID:
    浏览器发起WebSocket握手请求时,无法自定义携带Authorization请求头,你配置的AccessTokenProvider返回的token,SignalR默认会放到请求查询参数的access_token字段中,而服务端JWT认证默认只读取请求头中的token,导致身份认证失败,Context.User为空,自然拿不到用户ID。
  2. 长轮询模式下感知不到断开:
    长轮询的断开检测依赖超时机制,默认客户端异常断开后,服务端需要等待ClientTimeoutInterval时长(默认30秒)才会触发断开事件,如果超时配置不合理或者反向代理提前掐断了连接,就会出现断开事件不触发的问题,实时性远不如WebSocket。

最优解决方案(优先使用WebSocket)

步骤1:修正服务端Hub方法签名

你当前的OnConnectedAsync、OnDisconnectedAsync返回值写错了,多嵌套了一层Task,会导致异步执行异常、上下文丢失,先修正为正确的签名:

// 连接回调正确写法
public override async Task OnConnectedAsync()
{
    ConnectedUserModel connectedUserModel = new ConnectedUserModel() 
    { 
        ConnectionId = Context.ConnectionId, 
        UserId = _userManager.GetUserId(Context.User)
    };
    UserHandler.connectedUsers.Add(connectedUserModel);

    var temp = new List<string>();
    foreach(ConnectedUserModel connectedUser in UserHandler.connectedUsers)
    {
        if (!String.IsNullOrEmpty(connectedUserModel.UserId) && temp.Find(x=> x == connectedUserModel.UserId) == null)
        {
            temp.Add(connectedUserModel.UserId);
            await OnConnectAsync(connectedUserModel.UserId);
        }
    }
    await base.OnConnectedAsync();
}

// 断开回调正确写法
public override async Task OnDisconnectedAsync(Exception? exception)
{
    var connection =  UserHandler.connectedUsers.Find(x => x.ConnectionId == Context.ConnectionId);
    if (connection != null && !string.IsNullOrEmpty(connection.UserId))
    {
        await OnDisconnectAsync(connection.UserId);
        UserHandler.connectedUsers.Remove(connection);
    }
    await base.OnDisconnectedAsync(exception);
}

步骤2:配置服务端JWT认证支持查询参数读取

在JWT认证配置中添加OnMessageReceived事件,将SignalR请求中查询参数的access_token赋值给认证上下文:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // 保留你原有JWT配置,比如Issuer、Audience、校验规则等
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                // 替换为你实际的Hub路由前缀
                if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/signalr/hub"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

步骤3:修正客户端配置

删除手动添加Authorization头的代码,AccessTokenProvider已经可以自动适配不同传输方式处理token携带逻辑,手动加头会导致冲突:

HubConnection = new HubConnectionBuilder().WithUrl(Config.BaseUrl + ApplicationConstants.SignalR.HubUrl, options =>
{
    options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
    options.AccessTokenProvider = async () => await Task.FromResult(NPCompletApp.Token);
}).WithAutomaticReconnect().Build();

步骤4:确认中间件顺序与反向代理配置

  • 服务端中间件顺序必须为:UseRouting → UseAuthentication → UseAuthorization → MapHub,顺序错误会导致身份认证不生效。
  • 如果使用Nginx、IIS等反向代理,需要配置支持WebSocket协议转发,以Nginx为例:
location /signalr/hub {
    proxy_pass http://你的后端服务地址;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    # 调整超时时间避免WebSocket被提前断开
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

长轮询兼容方案(如确实无法使用WebSocket时选择)

调整SignalR超时配置保证断开事件正常触发:

builder.Services.AddSignalR(options =>
{
    // 客户端超时时间,超过该时间未收到客户端消息判定为断开
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    // 服务端心跳间隔,定时向客户端发送心跳包保活
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
});

该方案断开事件仍会有最多30秒的延迟,仅作为备选。


内容的提问来源于stack exchange,提问作者omar wasfi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.23 20:15:03