使用Azure Signal R Service后.NET 6 Blazor Server中HttpContext为空问题
我有一个.NET 6 Blazor Server应用,用于和.NET 6 Web Api通信。之前通过DelegatingHandler获取授权令牌并记录userId的逻辑运行正常,但改用Azure Signal R Service后,所有引用HttpContext的代码都出问题了。我已经在Startup中注册了IHttpContextAccessor并注入服务,且确保它的注册顺序在Signal R之前。
问题代码示例
AuthenticationDelegatingHandler实现:
public class AuthenticationDelegatingHandler : DelegatingHandler { private readonly ITokenAcquisition _tokenAcquisition; private readonly IConfiguration _configuration; private readonly ILogger<AuthenticationDelegatingHandler> _logger; private readonly IHttpContextAccessor _httpContext; public AuthenticationDelegatingHandler(ITokenAcquisition tokenAcquisition, IConfiguration configuration, ILogger<AuthenticationDelegatingHandler> logger, IHttpContextAccessor httpContext) { _tokenAcquisition = tokenAcquisition; _configuration = configuration; _logger = logger; _httpContext = httpContext; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var scopes = new[] { _configuration["ApiScope"] }; string accessToken = string.Empty; var userObject = _httpContext?.HttpContext?.User?.Claims.SingleOrDefault(_ => _.Type == ClaimTypes.NameIdentifier)?.Value; try { accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes!); _logger.LogInformation("Successfully retrieved access token from Azure for communication with API. User ObjectId - {UserObject}", userObject); } catch (Exception ex) { _logger.LogError(ex, "Error while fetching access token for communication with API. Details: {Message}. {InnerException}", ex.Message, ex.InnerException); throw; } request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var response = await base.SendAsync(request, cancellationToken); return response; } }
Startup中的注册代码:
builder.Services.AddRazorPages(); builder.Services.AddHttpContextAccessor(); builder.Services.AddServerSideBlazor() .AddMicrosoftIdentityConsentHandler(); builder.Services.AddScoped<NotificationService>(); builder.Services.AddTransient<AuthenticationDelegatingHandler>(); builder.Services.AddHttpClient("WebAPI", client => client.BaseAddress = new Uri(builder.Configuration["ApiBaseAddress"] ?? string.Empty)) .AddHttpMessageHandler<AuthenticationDelegatingHandler>();
核心原因
- Signal R连接的上下文特性:Blazor Server改用Azure Signal R后,应用从短HTTP请求模型切换为长连接的Signal R会话。
HttpContext仅在初始建立Signal R连接时存在,后续组件交互都在Signal R管道内进行,此时IHttpContextAccessor无法获取到有效的当前用户上下文——它依赖的是传统HTTP请求的上下文,而Signal R的后续消息不属于这个范畴。 - 生命周期不匹配:你的
AuthenticationDelegatingHandler是Transient(瞬时)生命周期,而HttpClient默认的创建逻辑可能脱离了Blazor组件的Scoped(作用域)上下文,导致它无法关联到当前Signal R连接对应的用户会话。
解决方案
方案1:替换HttpContextAccessor,改用AuthenticationStateProvider获取用户信息
在Blazor Server中,官方推荐用AuthenticationStateProvider来获取用户信息,而非HttpContextAccessor。你可以:
- 创建一个Scoped的用户信息服务,在组件初始化时把用户信息存入该服务,让
DelegatingHandler从这个Scoped服务中读取userId。
示例:创建Scoped服务存储用户信息
public class CurrentUserService { public string? UserObjectId { get; set; } } // 在Startup中注册为Scoped builder.Services.AddScoped<CurrentUserService>();
在Blazor组件中初始化:
@inject AuthenticationStateProvider AuthStateProvider @inject CurrentUserService CurrentUser @code { protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var user = authState.User; CurrentUser.UserObjectId = user.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; } }
修改AuthenticationDelegatingHandler,注入CurrentUserService替代IHttpContextAccessor:
public class AuthenticationDelegatingHandler : DelegatingHandler { private readonly ITokenAcquisition _tokenAcquisition; private readonly IConfiguration _configuration; private readonly ILogger<AuthenticationDelegatingHandler> _logger; private readonly CurrentUserService _currentUser; public AuthenticationDelegatingHandler(ITokenAcquisition tokenAcquisition, IConfiguration configuration, ILogger<AuthenticationDelegatingHandler> logger, CurrentUserService currentUser) { _tokenAcquisition = tokenAcquisition; _configuration = configuration; _logger = logger; _currentUser = currentUser; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var scopes = new[] { _configuration["ApiScope"] }; string accessToken = string.Empty; var userObject = _currentUser.UserObjectId; try { accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes!); _logger.LogInformation("Successfully retrieved access token from Azure for communication with API. User ObjectId - {UserObject}", userObject); } catch (Exception ex) { _logger.LogError(ex, "Error while fetching access token for communication with API. Details: {Message}. {InnerException}", ex.Message, ex.InnerException); throw; } request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var response = await base.SendAsync(request, cancellationToken); return response; } }
方案2:直接从ITokenAcquisition关联的用户上下文获取信息
ITokenAcquisition的GetAccessTokenForUserAsync方法本身依赖当前用户上下文,你可以尝试从它关联的用户Claims中直接获取userId,无需通过HttpContext:
// 在SendAsync方法中替换userObject的获取逻辑 var user = _tokenAcquisition.GetCurrentUser(); var userObject = user?.Claims.SingleOrDefault(_ => _.Type == ClaimTypes.NameIdentifier)?.Value;
方案3:确保HttpClient在组件Scoped上下文内使用
修改HttpClient的使用方式,在Blazor组件中通过IHttpClientFactory创建客户端,确保它运行在当前组件的Scoped上下文内,这样DelegatingHandler能关联到正确的用户会话:
@inject IHttpClientFactory HttpClientFactory @code { private async Task CallApi() { var client = HttpClientFactory.CreateClient("WebAPI"); // 调用API逻辑 } }
内容的提问来源于stack exchange,提问作者user2661305

