在PageModel的OnPost方法中使用Blazored.LocalStorage报错求助
问题场景
在Blazor的PageModel的OnPost方法中尝试通过Blazored.LocalStorage存储JWT(用于对接外部API),触发以下错误:
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.
无论是在OnPost中直接操作LocalStorage,还是调用CustomAuthenticationStateProvider的UpdateAuthenticationState方法间接操作,都会触发该错误。
相关代码
Login.cshtml
@page "/Login/{id:int?}" @model MyApp.Pages.LoginModel <body> <div id="login"> <form action="/Login" method="post" asp-page-handler="OnPost"> <label for="username">EMail</label> <input type="text" name="paramUsername" id="username" value="@Model.UserName"> </br> <label for="password">Password</label> <input type="text" name="paramPassword" id="password" type="password"> <button type="submit"> Envoyer </button> </form> </div> </body>
Login.cshtml.cs
public class LoginModel : PageModel { [Inject] public UtilisateurService utilisateurService { get; set; } [Inject] public AuthenticationStateProvider authStateProvider { get; set; } public string UserName { get; set; } public LoginModel(AuthenticationStateProvider authenticationStateProvider) { authStateProvider = authenticationStateProvider; } public async Task<IActionResult> OnPost(string paramUsername, string paramPassword) { var user = utilisateurService.Authenticate(paramUsername, paramPassword); if (user != null) { CustomAuthenticationStateProvider custom = (CustomAuthenticationStateProvider)authStateProvider; await custom.UpdateAuthenticationState(orqualClient); // 错误触发点 return LocalRedirect("/index"); } ModelState.AddModelError("Mdp", "Utilisateur ou mot de passe incorrect !"); return Page(); } }
CustomAuthenticationStateProvider.cs
public class CustomAuthenticationStateProvider : AuthenticationStateProvider { private readonly Blazored.LocalStorage.ILocalStorageService _localStorage; private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity()); private static string CLAIMS_IDENDITY_TYPE = "auth"; public CustomAuthenticationStateProvider(Blazored.LocalStorage.ILocalStorageService localStorage) { _localStorage = localStorage; } [...] public async Task UpdateAuthenticationState(User user) { ClaimsPrincipal claimsPrincipal; bool hasJwt = await _localStorage.GetItemAsync<string>("account_jwt") != null; // 错误触发点 if (hasJwt) await _localStorage.ClearAsync(); if (user != null && user.Jwt != "") { await _localStorage.SetItemAsync("account_jwt", user.Jwt); var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Email), }; foreach (var role in user.Roles) claims.Add(new Claim(ClaimTypes.Role, role)); var claimsIdentity = new ClaimsIdentity(claims, CLAIMS_IDENDITY_TYPE); claimsPrincipal = new ClaimsPrincipal(claimsIdentity); } else { claimsPrincipal = _anonymous; } NotifyAuthenticationStateChanged(Task.FromResult( new AuthenticationState(claimsPrincipal) )); } }
错误栈信息
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. Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime.BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId) MyApp.Services.CustomAuthenticationStateProvider.UpdateAuthenticationState(User user) in CustomAuthentificationStateProvider.cs var result = await _browserStorage.GetAsync<string>("account_jwt"); MyApp.Pages.LoginModel.OnPost(string paramUsername, string paramPassword) in Login.cshtml.cs await custom.UpdateAuthenticationState(orqualClient);
解决方案
核心原因
Blazored.LocalStorage依赖JavaScript互操作访问浏览器的LocalStorage,而PageModel的OnPost方法是在服务器端同步执行的,此时没有浏览器上下文,无法进行JS互操作。另外,预渲染模式下,只有OnAfterRenderAsync生命周期方法中才能安全执行JS互操作,因为此时组件已经完成渲染,浏览器环境就绪。
解决步骤
1. 优先方案:改用Blazor组件处理登录逻辑
将登录表单改为Blazor组件,把LocalStorage操作放在客户端执行,符合Blazor架构设计:
@page "/Login/{id:int?}" @inject UtilisateurService utilisateurService @inject CustomAuthenticationStateProvider authStateProvider @inject NavigationManager NavigationManager <EditForm Model="@loginModel" OnValidSubmit="@HandleLogin"> <DataAnnotationsValidator /> <div> <label for="username">EMail</label> <InputText @bind-Value="loginModel.Username" id="username" /> </div> <div> <label for="password">Password</label> <InputText type="password" @bind-Value="loginModel.Password" id="password" /> </div> <button type="submit">Envoyer</button> @if (!string.IsNullOrEmpty(errorMsg)) { <p style="color:red">@errorMsg</p> } </EditForm> @code { [Parameter] public int? Id { get; set; } private LoginModel loginModel = new(); private string errorMsg = string.Empty; private async Task HandleLogin() { var user = utilisateurService.Authenticate(loginModel.Username, loginModel.Password); if (user != null) { await authStateProvider.UpdateAuthenticationState(user); NavigationManager.NavigateTo("/index"); } else { errorMsg = "Utilisateur ou mot de passe incorrect !"; } } public class LoginModel { public string Username { get; set; } public string Password { get; set; } } }
2. 保留Razor Page的折中方案
如果必须使用Razor Page的OnPost,可通过Cookie中转JWT,再在客户端组件中完成LocalStorage存储:
- 修改
OnPost方法:
public async Task<IActionResult> OnPost(string paramUsername, string paramPassword) { var user = utilisateurService.Authenticate(paramUsername, paramPassword); if (user != null) { // 将JWT存入Cookie,允许客户端读取 Response.Cookies.Append("account_jwt", user.Jwt, new CookieOptions { HttpOnly = false, Secure = true, SameSite = SameSiteMode.Strict, Expires = DateTimeOffset.UtcNow.AddHours(2) }); return LocalRedirect("/index"); } ModelState.AddModelError("Mdp", "Utilisateur ou mot de passe incorrect !"); return Page(); }
- 在首页组件中处理Cookie转LocalStorage:
@inject Blazored.LocalStorage.ILocalStorageService localStorage @inject CustomAuthenticationStateProvider authStateProvider @inject UtilisateurService utilisateurService @code { protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { // 从Cookie读取JWT var jwt = await JSRuntime.InvokeAsync<string>("getCookie", "account_jwt"); if (!string.IsNullOrEmpty(jwt)) { // 获取用户信息(可从JWT解析或调用服务) var user = utilisateurService.GetUserFromJwt(jwt); await authStateProvider.UpdateAuthenticationState(user); // 存入LocalStorage await localStorage.SetItemAsync("account_jwt", jwt); // 清理Cookie await JSRuntime.InvokeVoidAsync("deleteCookie", "account_jwt"); } } await base.OnAfterRenderAsync(firstRender); } } <script> // 操作Cookie的JS方法 function getCookie(name) { let value = "; " + document.cookie; let parts = value.split("; " + name + "="); if (parts.length === 2) return parts.pop().split(";").shift(); } function deleteCookie(name) { document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`; } </script>
3. 禁用预渲染(不推荐)
若不考虑首屏性能,可禁用预渲染绕过限制:
- 修改
Program.cs:
builder.Services.AddServerSideBlazor(options => { options.DetailedErrors = true; });
- 修改
_Host.cshtml的组件渲染模式:
<component type="typeof(App)" render-mode="Server" />
注意事项
- 服务器端无法直接访问浏览器LocalStorage,所有LocalStorage操作必须在客户端执行。
- 使用Cookie传递JWT时,务必配置
Secure、SameSite等安全属性,避免XSS或CSRF风险。 - 优先采用Blazor组件处理客户端逻辑,这是Blazor框架的设计初衷。
内容的提问来源于stack exchange,提问作者angela

