Blazor Server(.NET6)中SignalR的HubConnectionContext二次调用丢失用户信息
问题描述
基于Blazor Server和.NET6实现了默认身份认证功能(登录、注销、注册正常),尝试通过SignalR向特定用户发送消息时遇到问题:已实现IUserIdProvider接口获取UserId,首次启动应用时可通过connection.GetHttpContext()或connection.User.FindFirstValue(ClaimTypes.Name)获取用户信息,但导航到其他页面再次调用Hub时,HubConnectionContext丢失了用户所有信息。强制使用常量ID可正常工作,疑惑二次调用丢失信息的原因,不确定是否需要配置Cookie(首次调用可正常获取信息)。
现有代码
CustomUserIdProvider.cs
public class CustomUserIdProvider : IUserIdProvider { public string? GetUserId(HubConnectionContext connection) { var httpContext = connection.GetHttpContext(); var userId = connection.User.FindFirstValue(ClaimTypes.Name); if (string.IsNullOrWhiteSpace(userId)) return string.Empty; return userId; } }
Program.cs
// ... builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>(); // ... app.UseAuthentication(); app.UseAuthorization();
SignalR.razor
protected override async Task OnInitializedAsync() { hubConnection = new HubConnectionBuilder() .WithUrl(NavigationManager.ToAbsoluteUri("/notifyhub")) .Build(); hubConnection.On<int, string>("ReceiveMessage", (id, message) => { var encodedMsg = $"{id}: {message}"; InvokeAsync(StateHasChanged); }); await hubConnection.StartAsync(); } private async Task Send() { if (hubConnection is not null) { var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var user = authState.User; authMessage = $"{user.Identity.Name} is authenticated."; claims = user.Claims; surnameMessage = $"Surname: {user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value}"; await hubConnection.SendAsync("Send", user.Identity.Name, 1, "Message envoyé"); } }
问题原因及解决方法
核心原因
Blazor Server采用SPA式客户端导航,而非传统HTTP页面刷新。首次加载时,SignalR连接基于初始请求的HttpContext建立,自动携带了认证Cookie;但导航后重新创建的HubConnection默认不会主动携带认证凭证,导致Hub端无法识别用户身份。
解决步骤
配置HubConnection携带认证凭证
在构建HubConnection时,显式设置包含Cookie凭证,确保认证信息被传递:hubConnection = new HubConnectionBuilder() .WithUrl(NavigationManager.ToAbsoluteUri("/notifyhub"), options => { options.Credentials = FetchCredentials.Include; }) .WithAutomaticReconnect() // 可选:添加自动重连,提升连接稳定性 .Build();确认中间件顺序正确性
确保Program.cs中中间件顺序符合.NET认证规范,必须按以下顺序配置:app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapBlazorHub(); // 必须在认证中间件之后注册 app.MapFallbackToPage("/_Host");优化用户ID获取逻辑
建议使用ClaimTypes.NameIdentifier作为用户唯一标识(而非ClaimTypes.Name),避免用户名重复导致的问题:public string? GetUserId(HubConnectionContext connection) { var userId = connection.User.FindFirstValue(ClaimTypes.NameIdentifier); return string.IsNullOrWhiteSpace(userId) ? string.Empty : userId; }复用HubConnection避免重复创建
将HubConnection封装为注入服务,避免每次组件初始化都创建新连接:// 封装Hub服务类 public class NotifyHubService { public HubConnection HubConnection { get; } public NotifyHubService(NavigationManager navigationManager) { HubConnection = new HubConnectionBuilder() .WithUrl(navigationManager.ToAbsoluteUri("/notifyhub"), options => { options.Credentials = FetchCredentials.Include; }) .WithAutomaticReconnect() .Build(); } } // Program.cs中注册服务 builder.Services.AddScoped<NotifyHubService>(); // 组件中注入使用 [Inject] private NotifyHubService HubService { get; set; } protected override async Task OnInitializedAsync() { if (HubService.HubConnection.State == HubConnectionState.Disconnected) { await HubService.HubConnection.StartAsync(); } HubService.HubConnection.On<int, string>("ReceiveMessage", (id, message) => { var encodedMsg = $"{id}: {message}"; InvokeAsync(StateHasChanged); }); }
内容的提问来源于stack exchange,提问作者Dmidecode

