Blazor Server:无法从HttpMessageHandler访问ProtectedSessionStorage值
问题概述
开发调用外部API的Blazor Server应用时,登录后将JWT令牌存入ProtectedSessionStorage,Razor组件中能正常获取令牌,但自定义DelegatingHandler中调用获取令牌时抛出异常:
System.InvalidOperationException: '此时无法发出JavaScript互操作调用。这是因为组件正在进行静态渲染。启用预渲染时,只能在OnAfterRenderAsync生命周期方法中执行JavaScript互操作调用。'
原因分析
Blazor Server默认启用预渲染,在预渲染阶段(组件初始化早期),服务器尚未与客户端建立JS连接,无法执行JS互操作。ProtectedSessionStorage依赖浏览器sessionStorage,必须通过JS互操作访问;若DelegatingHandler在预渲染阶段被触发(比如组件在OnInitializedAsync中调用HttpClient),就会触发该异常。
解决方案
方案1:延迟HttpClient调用至OnAfterRenderAsync阶段
组件中避免在OnInitializedAsync等预渲染阶段调用HttpClient,移至OnAfterRenderAsync(该阶段JS环境已就绪):
@inject IHttpClientFactory HttpClientFactory @inject TokenService tokenService <p>@message</p> @code { string message = ""; bool _hasRendered = false; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && !_hasRendered) { _hasRendered = true; // 此时JS互操作可用,可安全调用HttpClient var httpClient = HttpClientFactory.CreateClient("AuthenticatedClient"); var response = await httpClient.GetAsync("/api/data"); if (response.IsSuccessStatusCode) { message = "授权成功"; var result = await response.Content.ReadAsStringAsync(); } else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { message = "未授权"; } StateHasChanged(); } await base.OnAfterRenderAsync(firstRender); } }
方案2:将令牌存储在服务器端(推荐)
避免依赖客户端存储,登录后将令牌存入服务器端HttpContext.Session或用户ClaimsPrincipal中,DelegatingHandler无需JS互操作即可获取令牌:
修改TokenService,从ClaimsPrincipal获取令牌
public class TokenService(IHttpContextAccessor httpContextAccessor) { public string GetToken() { var httpContext = httpContextAccessor.HttpContext; if (httpContext == null) return string.Empty; // 假设登录时已将令牌存入Claims var tokenClaim = httpContext.User.Claims.FirstOrDefault(c => c.Type == "AccessToken"); return tokenClaim?.Value ?? string.Empty; } }
更新AuthTokenHandler
public class AuthTokenHandler(TokenService tokenService) : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = tokenService.GetToken();// 无需异步,避免JS互操作 if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } return await base.SendAsync(request, cancellationToken); } }
服务注册更新
// 添加HttpContextAccessor builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped<TokenService>(); builder.Services.AddScoped<AuthTokenHandler>(); builder.Services.AddHttpClient("AuthenticatedClient", client => { client.BaseAddress = new Uri(BaseUrl); }).AddHttpMessageHandler<AuthTokenHandler>();
方案3:禁用预渲染
修改_Host.cshtml,移除预渲染标记:
<!-- 原代码 --> <component type="typeof(App)" render-mode="ServerPrerendered" /> <!-- 修改后 --> <component type="typeof(App)" render-mode="Server" />
此方法简单,但会导致首屏加载变慢,需等待Blazor连接建立后才渲染内容。
总结
推荐使用方案2,将令牌存储在服务器端,既规避JS互操作限制,也提升安全性(令牌不暴露给客户端)。若必须使用客户端存储,则采用方案1确保HttpClient调用在JS环境就绪后执行。
内容的提问来源于stack exchange,提问作者Anuj Karki

