You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor Server自定义认证:登录成功后无法获取用户认证状态

问题:Blazor Server自定义Cookie认证后无法获取已认证用户状态

我正在将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");
    }
}

问题分析与修复方案

核心问题

  1. HttpContext访问逻辑错误:Blazor Server中,HttpContextAccessor.HttpContext仅在初始页面加载的HTTP请求中可用,后续组件交互通过SignalR连接,无法直接获取当前请求的HttpContext。你的CustomAuthenticationStateProvider直接依赖它,导致登录后的SignalR连接无法读取到更新后的认证状态。
  2. 认证状态通知无效:登录成功后调用GetAuthenticationStateAsync()时,HttpContext还未同步更新(API请求和Blazor组件上下文分离),因此通知的还是未认证状态。
  3. 缺少本地状态存储: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 11:19:51