Blazor Server读取浏览器本地存储时JavaScript互操作异常排查
.NET 9 Blazor Server + Refit 授权令牌获取时的JavaScript互操作异常问题
问题背景
使用.NET 9.0开发Blazor Server应用,通过Refit调用需要JWT授权的API,令牌存储在ProtectedLocalStorage中,需通过刷新令牌自动更新。尝试两种方案实现授权头注入,但均触发以下异常:
System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.
异常不仅出现在OnAfterRenderAsync中,按钮点击事件调用API时也会触发。奇怪的是,直接在OnAfterRenderAsync中调用令牌服务的GetAccessToken()正常,但通过Refit的回调(AuthorizationHeaderValueGetter或DelegatingHandler)调用则报错。
问题原因
- 预渲染阶段限制:Blazor Server默认启用预渲染,此阶段无法执行JavaScript互操作(
ProtectedLocalStorage依赖JS互操作)。 - Refit回调时机:Refit的授权回调逻辑可能在组件预渲染阶段就被触发(比如客户端初始化、请求调度),即使API调用是在
OnAfterRenderAsync或按钮点击中执行。 - 静态类不匹配Scoped生命周期:第一种方案中使用静态类
AuthenticationTokenProvider获取令牌,静态类的生命周期与Blazor的Scoped服务不兼容,导致在预渲染阶段尝试访问ProtectedLocalStorage。
解决方案
1. 将令牌提供者改为Scoped服务,添加预渲染检查
放弃静态类,改用Scoped服务管理令牌获取,确保所有依赖ProtectedLocalStorage的操作只在交互式渲染阶段执行:
public interface IAuthenticationTokenProvider { Task<string> GetTokenAsync(); } public class AuthenticationTokenProvider : IAuthenticationTokenProvider { private readonly IAuthenticationService _authService; private readonly NavigationManager _navigationManager; public AuthenticationTokenProvider(IAuthenticationService authService, NavigationManager navigationManager) { _authService = authService; _navigationManager = navigationManager; } public async Task<string> GetTokenAsync() { // 跳过预渲染阶段的令牌获取 if (_navigationManager.IsPreRendering) return string.Empty; var accessToken = await _authService.GetAccessToken(); if (IsTokenValid(accessToken)) return accessToken; var refreshToken = await _authService.GetRefreshToken(); if (string.IsNullOrEmpty(refreshToken)) return string.Empty; var newTokens = await _authService.RefreshTokens(refreshToken); if (newTokens != null) { await _authService.SaveTokens(newTokens); return newTokens.AccessToken; } return string.Empty; } // 实现JWT有效性校验逻辑 private bool IsTokenValid(string token) { if (string.IsNullOrEmpty(token)) return false; var handler = new JwtSecurityTokenHandler(); if (!handler.CanReadToken(token)) return false; var jwtToken = handler.ReadJwtToken(token); return jwtToken.ValidTo > DateTime.UtcNow; } }
2. 修改AuthenticationService,添加预渲染检查
确保ProtectedLocalStorage的操作仅在交互式阶段执行:
public class AuthenticationService : IAuthenticationService { private readonly ProtectedLocalStorage _localStorage; private readonly NavigationManager _navigationManager; public AuthenticationService(ProtectedLocalStorage localStorage, NavigationManager navigationManager) { _localStorage = localStorage; _navigationManager = navigationManager; } public async Task<string> GetAccessToken() { if (_navigationManager.IsPreRendering) return string.Empty; var result = await _localStorage.GetAsync<string>("accessToken"); return result.Success ? result.Value : string.Empty; } // 同理为GetRefreshToken、SaveTokens、RefreshTokens方法添加预渲染检查 }
3. 使用DelegatingHandler注入授权头(推荐方案)
注册Scoped的AuthenticationHandler,注入IAuthenticationTokenProvider实现授权头动态添加:
public class AuthenticationHandler : DelegatingHandler { private readonly IAuthenticationTokenProvider _tokenProvider; public AuthenticationHandler(IAuthenticationTokenProvider tokenProvider) { _tokenProvider = tokenProvider; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = await _tokenProvider.GetTokenAsync(); if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } return await base.SendAsync(request, cancellationToken); } }
4. 注册所有服务
在Program.cs中注册Scoped服务和Refit客户端:
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>(); builder.Services.AddScoped<IAuthenticationTokenProvider, AuthenticationTokenProvider>(); builder.Services.AddScoped<AuthenticationHandler>(); // 注册Refit客户端并添加授权Handler builder.Services.AddRefitClient<IMyApiClient>() .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://your-api-base-url.com")) .AddHttpMessageHandler<AuthenticationHandler>();
5. 组件中确保交互式阶段调用API
在组件中,可通过NavigationManager.IsPreRendering判断当前是否为交互式渲染,避免预渲染阶段触发API调用:
@inject IMyApiClient ApiClient @inject NavigationManager NavManager <button @onclick="CallProtectedApi">调用授权API</button> @code { private async Task CallProtectedApi() { try { var data = await ApiClient.GetProtectedResource(); // 处理返回数据 } catch (Exception ex) { // 异常处理逻辑 } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && !NavManager.IsPreRendering) { try { var data = await ApiClient.GetProtectedResource(); // 处理返回数据 } catch (Exception ex) { // 异常处理逻辑 } } } }
关键说明
- 按钮点击事件属于交互式操作,预渲染阶段不会触发,因此无需额外检查即可安全调用API。
OnAfterRenderAsync中需结合firstRender和IsPreRendering确保仅在第一次交互式渲染后执行API调用。- 禁用预渲染是临时解决方案,但会失去预渲染带来的SEO和首屏加载优势,不推荐使用。
内容的提问来源于stack exchange,提问作者MatS2510
相关产品推荐
相关产品推荐

