Blazor Server自定义认证:登录成功后无法获取用户认证状态
我正在将React/Java应用迁移至Blazor Server,计划用Cookie认证替代原Java应用的JWT Token,同时保留API支持未来扩展。后端采用Service->Controller架构,前端通过调用API完成认证。登录API返回成功,但CustomAuthenticationStateProvider始终无法获取已认证用户,Home页判定用户未认证,请求排查问题。
相关代码
Users实体类
[Table("USERS")] public class Users { [Key] [Column("id")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } [Column("created_by")] public string? CreatedBy { get; set; } [Column("created_date")] public DateTime? CreatedDate { get; set; } [Column("last_modified_by")] public string? LastModifiedBy { get; set; } [Column("last_modified_date")] public DateTime? LastModifiedDate { get; set; } [Column("activated")] public bool? Activated { get; set; } [Column("email")] public String? Email { get; set; } [Column("integration_token")] public String? IntegrationToken { get; set; } [Column("password")] public String Password { get; set; } [Column("user_system")] public String? UserSystem { get; set; } [Column("username")] public String Username { get; set; } [Column("iw_account_id")] public long? IwAccountId { get; set; } [Column("scheduled_time")] public String? ScheduledTime { get; set; } [Column("scheduler_enabled")] public bool? SchedulerEnabled { get; set; } [Column("sso_enabled")] public bool? SsoEnabled { get; set; } [Column("use_credit_invoice")] public bool? UseCreditInvoice { get; set; } [Column("extra_fee_percentage")] public double? ExtraFeePercentage { get; set; } [Column("use_all_in_one_invoice")] public bool? UseAllInOneInvoice { get; set; } }
UserRoles实体类
[Table("USER_ROLES")] public class UserRoles { [Key, Column("user_id", Order = 0)][ForeignKey("Users")] public long UserId { get; set; } [ForeignKey("Roles")][Column("role_id")] public long RoleId { get; set; } public Users Users { get; set; } public Roles Roles { get; set; } }
AuthService.cs核心方法
public async Task<bool> ValidateCredentials(string username, string password) { var user = await _userRepository.FindByUsernameIgnoreCaseAsync(username); if (user == null) return false; return BCrypt.Net.BCrypt.Verify(password, user.Password); } public async Task<IEnumerable<Claim>> GetClaimsForUser(Users user) { var roles = await _userRepository.GetUserRoleNamesAsync(user.Id); var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) }; foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } return claims; }
CustomAuthenticationStateProvider.cs
public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var user = _httpContextAccessor.HttpContext?.User; _logger.LogInformation($"User: {user.Identity.Name}"); if (user?.Identity?.IsAuthenticated ?? false) { return new AuthenticationState(user); } return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } public new void NotifyAuthenticationStateChanged(Task<AuthenticationState> task) { _logger.LogInformation("Notify Authentication State Changed"); base.NotifyAuthenticationStateChanged(task); }
AuthController.cs登录接口
[HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginRequest model) { if (!(await _authService.ValidateCredentials(model.Username, model.Password))) { return Unauthorized(new { message = "Login unsuccessfully" }); } var user = await _userRepository.FindByUsernameIgnoreCaseAsync(model.Username); var claims = await _authService.GetClaimsForUser(user); var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTime.UtcNow.AddMinutes(30) }; await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); return Ok(new { message = "Login successfully", user = new { user.Id, user.Username } }); }
Login.razor核心逻辑
@page "/" @code { private async Task LoginUser() { try { Logger.LogInformation("Attempting to log in user: {Username}", Input.Username); var response = await Http.PostAsJsonAsync("/api/authenticate/login", Input); var responseContent = await response.Content.ReadAsStringAsync(); Logger.LogInformation("API response status code: {StatusCode}", response.StatusCode); Logger.LogInformation("API response content: {Content}", responseContent); if (response.IsSuccessStatusCode) { await AuthStateProvider.GetAuthenticationStateAsync(); AuthStateProvider.NotifyAuthenticationStateChanged( Task.FromResult(await AuthStateProvider.GetAuthenticationStateAsync()) ); await Task.Delay(2000); NavigationManager.NavigateTo("/", true); } else { StateHasChanged(); } } catch (Exception ex) { Logger.LogError(ex, "An error occurred during login for user: {Username}", Input.Username); errorMessage = "An unexpected error occurred."; } } }
Home.razor认证检查逻辑
protected override async Task OnParametersSetAsync() { await Task.Delay(1000); Logger.LogInformation("Start to get authenticate state async"); var authState = await CustomAuthStateProvider.GetAuthenticationStateAsync(); Logger.LogInformation($"authState at home.razor: {authState.User.Identity.IsAuthenticated}"); if (!authState.User.Identity.IsAuthenticated) { NavigationManager.NavigateTo("/login"); } }
问题分析与修复方案
核心问题
- HttpContext访问逻辑错误:Blazor Server中,
HttpContextAccessor.HttpContext仅在初始页面加载的HTTP请求中可用,后续组件交互通过SignalR连接,无法直接获取当前请求的HttpContext。你的CustomAuthenticationStateProvider直接依赖它,导致登录后的SignalR连接无法读取到更新后的认证状态。 - 认证状态通知无效:登录成功后调用
GetAuthenticationStateAsync()时,HttpContext还未同步更新(API请求和Blazor组件上下文分离),因此通知的还是未认证状态。 - 缺少本地状态存储:
AuthenticationStateProvider需要内部存储认证状态,而非每次都从外部读取。
修复步骤
1. 重构CustomAuthenticationStateProvider
改为内部存储认证状态,提供手动更新方法:
public class CustomAuthenticationStateProvider : AuthenticationStateProvider { private readonly ILogger<CustomAuthenticationStateProvider> _logger; private ClaimsPrincipal _currentUser = new ClaimsPrincipal(new ClaimsIdentity()); public CustomAuthenticationStateProvider(ILogger<CustomAuthenticationStateProvider> logger) { _logger = logger; } public override Task<AuthenticationState> GetAuthenticationStateAsync() { _logger.LogInformation($"Current User: {_currentUser.Identity.Name}"); return Task.FromResult(new AuthenticationState(_currentUser)); } public void UpdateAuthenticationState(ClaimsPrincipal user) { _currentUser = user; _logger.LogInformation("Authentication state updated"); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public void ClearAuthenticationState() { _currentUser = new ClaimsPrincipal(new ClaimsIdentity()); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }
2. 修改AuthController返回Claims信息
登录接口需要返回用户的Claims,供前端创建认证主体:
[HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginRequest model) { if (!(await _authService.ValidateCredentials(model.Username, model.Password))) { return Unauthorized(new { message = "登录失败" }); } var user = await _userRepository.FindByUsernameIgnoreCaseAsync(model.Username); var claims = await _authService.GetClaimsForUser(user); var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTime.UtcNow.AddMinutes(30) }; await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); // 返回Claims给前端 return Ok(new { message = "登录成功", user = new { user.Id, user.Username }, claims = claims.Select(c => new { c.Type, c.Value }) }); }
3. 调整Login.razor登录逻辑
登录成功后,根据返回的Claims创建认证主体并更新状态:
@page "/login" @inject CustomAuthenticationStateProvider CustomAuthStateProvider @inject HttpClient Http @inject NavigationManager NavigationManager @inject ILogger<Login> Logger <EditForm Model="@Input" OnValidSubmit="@LoginUser"> <!-- 登录表单UI --> </EditForm> @code { private LoginRequest Input { get; set; } = new LoginRequest(); private string? errorMessage; private async Task LoginUser() { try { Logger.LogInformation("尝试登录用户: {Username}", Input.Username); var response = await Http.PostAsJsonAsync("/api/authenticate/login", Input); var responseContent = await response.Content.ReadFromJsonAsync<LoginResponse>(); Logger.LogInformation("API响应状态码: {StatusCode}", response.StatusCode); if (response.IsSuccessStatusCode && responseContent != null) { // 根据返回的Claims创建认证主体 var claims = responseContent.Claims.Select(c => new Claim(c.Type, c.Value)); var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var user = new ClaimsPrincipal(claimsIdentity); // 更新认证状态 CustomAuthStateProvider.UpdateAuthenticationState(user); // 跳转首页 NavigationManager.NavigateTo("/", true); } else { errorMessage = responseContent?.message ?? "登录失败"; } } catch (Exception ex) { Logger.LogError(ex, "登录过程中发生错误,用户: {Username}", Input.Username); errorMessage = "发生未知错误"; } } // 响应模型定义 public class LoginResponse { public string Message { get; set; } = string.Empty; public UserInfo? User { get; set; } public List<ClaimInfo> Claims { get; set; } = new List<ClaimInfo>(); } public class UserInfo { public long Id { get; set; } public string Username { get; set; } = string.Empty; } public class ClaimInfo { public string Type { get; set; } = string.Empty; public string Value { get; set; } = string.Empty; } public class LoginRequest { public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } }
4. 优化Home.razor认证检查
去掉不必要的延迟,直接依赖更新后的认证状态:
@page "/" @inject CustomAuthenticationStateProvider CustomAuthStateProvider @inject NavigationManager NavigationManager @inject ILogger<Home> Logger @code { protected override async Task OnInitializedAsync() { Logger.LogInformation("开始获取认证状态"); var authState = await CustomAuthStateProvider.GetAuthenticationStateAsync(); Logger.LogInformation("Home页认证状态: {IsAuthenticated}", authState.User.Identity.IsAuthenticated); if (!authState.User.Identity.IsAuthenticated) { NavigationManager.NavigateTo("/login"); } } }
5. 确保Program.cs配置正确
添加Cookie认证服务并注入自定义状态提供者:
// Program.cs builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.ExpireTimeSpan = TimeSpan.FromMinutes(30); options.SlidingExpiration = true; // 生产环境需设置Secure=true // options.Cookie.SecurePolicy = CookieSecurePolicy.Always; }); builder.Services.AddAuthorization(); builder.Services.AddScoped<CustomAuthenticationStateProvider>(); builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthenticationStateProvider>());
补充说明
- Blazor Server的SignalR连接无法直接访问HttpContext,因此必须将认证状态存储在
AuthenticationStateProvider内部。 - 登录成功后,通过API返回Claims并手动更新状态,是实现前后端认证同步的可靠方式。
- 生产环境需根据部署配置调整Cookie的
Secure、SameSite等属性,确保安全性。
内容的提问来源于stack exchange,提问作者Oscar
相关产品推荐
相关产品推荐

