.NET 8 Blazor Web App登录异常求助:控制器登录成功但AuthorizeView未生效
问题背景
使用.NET 8 Blazor Web App框架,基于Microsoft.AspNetCore.Identity.EntityFramework实现认证。因InteractiveServer渲染模式不支持直接使用HttpContext/SignInManager/UserManager,通过向控制器发送JSON请求实现登录功能,注册功能已成功实现(未包含自动登录),但登录后AuthorizeView仍显示未登录状态。断点调试确认控制器中SignInManager.PasswordSignInAsync返回登录成功,尝试自定义AuthenticationStateProvider更新认证状态也未解决问题。
现有代码片段
Razor登录页面代码
@inject IHttpClientFactory _HttpClientFactory @inject IStringLocalizer<Login> _Localizer @inject NavigationManager _NavigationManager @inject IMessageService _Message <EditForm Model="_Input" method="post" OnValidSubmit="LoginUser" FormName="login" Enhance class="Flex"> <DataAnnotationsValidator /> <div id="SubmitDiv" class="FlexAll"> <InputText @bind-Value="_Input.Email" class="ant-input SubmitContentInput" autocomplete="username" aria-required="true" placeholder="@_Localizer["ErrorMessageUserNameRequire"]" /> <ValidationMessage For="() => _Input.Email" class="ant-form-item-explain-error" /> <InputText @bind-Value="_Input.Password" type="password" class="ant-input SubmitContentInput" autocomplete="current-password" aria-required="true" placeholder="@_Localizer["ErrorMessagePasswordRequire"]" /> <ValidationMessage For="() => _Input.Password" class="ant-form-item-explain-error" /> </div> <button type="submit" class="ant-btn ant-btn-primary Flex">@_Localizer["Login"]</button> </EditForm> [SupplyParameterFromForm] private LoginModel _Input { get; set; } = new(); public async Task LoginUser() { var httpClient = _HttpClientFactory.CreateClient(); var httpResponseMessage = await httpClient.PostAsJsonAsync<LoginModel>($"{_NavigationManager.BaseUri}Login/Login", this._Input); if (httpResponseMessage.IsSuccessStatusCode) { var result = await httpResponseMessage.Content.ReadFromJsonAsync<LoginResultModel>(); if (result is { }) { switch (result.LoginResultType) { case LoginResultModel.LoginResultTypeEnum.Success: { _NavigationManager.NavigateTo("/"); return; } } } else { await _Message.Error(_Localizer["UnableToLogInNormally"].Value); } } }
登录控制器代码
[Microsoft.AspNetCore.Mvc.Route("[controller]/[action]")] public class LoginController: Controller { readonly SignInManager<ApplicationUser> _SignInManager; readonly ILogger<Login> _Logger; public LoginController(SignInManager<ApplicationUser> signInManager, ILogger<Login> logger) { _SignInManager = signInManager; _Logger = logger; } [HttpPost] [Produces("application/json")] public async Task<ActionResult<LoginResultModel>> Login([FromBody] LoginModel login) { LoginResultModel _loginResult = new LoginResultModel(); var result = await _SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { _Logger.LogInformation("User logged in."); _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.Success; return _loginResult; } else if (result.RequiresTwoFactor) { _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.RequiresTwoFactor; _loginResult.QueryParameter = new() { ["rememberMe"] = login.RememberMe }; return _loginResult; } else if (result.IsLockedOut) { _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.LockedOut; return _loginResult; } else { _loginResult.LoginResultType = LoginResultModel.LoginResultTypeEnum.InvalidLoginAttempt; return _loginResult; } } }
AuthorizeView代码
<AuthorizeView> <Authorized> <div class="page"> <div class="sidebar"> <NavMenu /> </div> <main> <div class="top-row px-4"> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">@_Localizer["Test"]</a> </div> <article class="content px-4"> @Body </article> </main> </div> </Authorized> <NotAuthorized> <Login></Login> </NotAuthorized> </AuthorizeView>
自定义AuthenticationStateProvider代码
public class CustomAuthenticationStateProvider : AuthenticationStateProvider { public override Task<AuthenticationState> GetAuthenticationStateAsync() { var _user = new ClaimsPrincipal(new ClaimsIdentity()); return Task.FromResult(new AuthenticationState(_user)); } public void MarkUserAsAuthored(string eMail) { var _identity= new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name,eMail) }, "apiauth_type"); var _user = new ClaimsPrincipal(_identity); NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_user))); } }
问题根源
认证状态不同步:
控制器中SignInManager.PasswordSignInAsync成功后,仅向HTTP响应写入认证Cookie,但Blazor InteractiveServer的SignalR连接独立于HTTP请求,当前已建立的连接不会自动读取新生成的Cookie,因此AuthorizeView仍使用连接建立时的未认证状态。自定义AuthenticationStateProvider的缺陷:
- 控制器调用
MarkUserAsAuthored无效:控制器与Blazor组件处于不同依赖注入作用域,二者的AuthenticationStateProvider实例不共享,控制器的调用无法通知到Blazor组件的状态监听。 - 自定义Provider默认返回匿名用户,未读取Identity认证Cookie,即使临时修改状态,页面刷新后也会恢复为未认证状态。
- 控制器调用
解决方案
方案一:强制页面刷新(快速解决)
登录成功后,使用forceLoad: true触发全页面刷新,重新发起HTTP请求并携带登录后的认证Cookie,Blazor会重新建立SignalR连接并获取正确的认证状态:
case LoginResultModel.LoginResultTypeEnum.Success: { _NavigationManager.NavigateTo("/", forceLoad: true); return; }
方案二:正确实现自定义AuthenticationStateProvider
注册自定义Provider:
在Program.cs中替换默认的AuthenticationStateProvider:builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();修改自定义Provider,读取认证Cookie:
注入IHttpContextAccessor读取当前请求的认证Cookie,确保初始状态正确:public class CustomAuthenticationStateProvider : AuthenticationStateProvider { private readonly IHttpContextAccessor _httpContextAccessor; public CustomAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public override Task<AuthenticationState> GetAuthenticationStateAsync() { var user = _httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(new ClaimsIdentity()); return Task.FromResult(new AuthenticationState(user)); } public void MarkUserAsAuthored(ClaimsPrincipal user) { NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); } }在Razor页面中更新状态:
登录成功后,调用自定义Provider的方法更新状态:// 在LoginUser方法的Success分支 case LoginResultModel.LoginResultTypeEnum.Success: { var authStateProvider = (CustomAuthenticationStateProvider)_AuthenticationStateProvider; authStateProvider.MarkUserAsAuthored(new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, _Input.Email) }, "apiauth_type"))); _NavigationManager.NavigateTo("/"); return; }
方案三:使用Blazor内置认证流程
若业务允许,可切换到InteractiveWebAssembly模式,或使用Blazor的RemoteAuthenticationService处理认证,避免手动控制器登录的状态同步问题。
内容的提问来源于stack exchange,提问作者Melon NG

