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

.NET 8 Blazor Server API认证异常:AuthState始终为False

问题:API认证成功且调用HttpContext.SignInAsync后,Blazor的AuthState始终为False

在Blazor应用中,API认证流程已成功执行,后端也调用HttpContext.SignInAsync写入了认证Cookie,但前端的AuthState始终显示未认证(IsAuthenticated = false),AuthState.User始终为null。此前.NET 8版本前,使用自定义WebAuthStateProvider更新用户状态,但当前该方式未生效。


相关代码片段

自定义WebAuthStateProvider

public class WebAuthStateProvider(IHttpContextAccessor httpContextAccessor)
    : AuthenticationStateProvider, IAuthProvider
{
    private ClaimsPrincipal user = httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        return Task.FromResult(new AuthenticationState(user));
    }

    public async Task SaveUser(IEnumerable<Claim> claims)
    {
        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        user = new(identity);
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
    }

    public async Task Logout()
    {
        user = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
    }

    public void NotifyUserAuthentication()
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}

Login.razor登录逻辑

private async Task LoginWithEmail()
{
    errorMessage = null;

    var loginRequest = new LoginRequestModel()
    {
        Platform = IsWeb ? "web" : "mobile",
        Email = email,
        Password = password
    };

    try
    {
        var baseUrl = IsWeb ? Navigation.BaseUri : "https://mydomain/";
        var requestUrl = $"{baseUrl}api/v1/auth/login";
        var response = await Http.PostAsJsonAsync(requestUrl, loginRequest);

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            errorMessage = "Invalid email or password.";
            return;
        }

        if (!response.IsSuccessStatusCode)
        {
            errorMessage = "An error occurred while logging in. Please try again.";
            return;
        }

        var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>();

        if (authResponse != null)
        {
            var claims = new List<Claim>
            {
                new(ClaimTypes.Email, authResponse.Email),
                new("Avatar", authResponse.Avatar),
                new("Id", authResponse.Id),
                new("JWT", authResponse.Token),
            };
            claims.AddRange(authResponse.Roles.Select(role => new Claim(ClaimTypes.Role, role)));

            var test = AuthState.User; // 始终为null(IsAuthenticated = false)

            Navigation.NavigateTo("/feed", true);
        }
    }
    catch
    {
        errorMessage = "An unexpected error occurred. Please try again later.";
    }
}

Program.cs认证配置

builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
    {
        options.LoginPath = "/identity/login";
        options.LogoutPath = "/identity/logout";
        options.AccessDeniedPath = "/identity/access-denied";
        options.ExpireTimeSpan = TimeSpan.FromDays(7);
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.TokenValidationParameters = new()
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["AuthConfiguration:jwtTokenConfig:issuer"],
            ValidAudience = builder.Configuration["AuthConfiguration:jwtTokenConfig:issuer"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["AuthConfiguration:jwtTokenConfig:secret"])
            )
        };
    })
    .AddGoogle("Google", options =>
    {
        options.ClientId = builder.Configuration["Google:ClientId"];
        options.ClientSecret = builder.Configuration["Google:ClientSecret"];
        options.ClaimActions.MapJsonKey("urn:google:profile", "link");
        options.ClaimActions.MapJsonKey("urn:google:image", "picture");
        options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
        options.SaveTokens = true;
    });

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();
...

app.UseAuthentication();
app.UseAuthorization();
app.UseRouting();

AuthController.cs注册接口

[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequestModel requestModel)
{
    if (await userManager.FindByEmailAsync(requestModel.Email) != null)
        return Conflict("User with this email already exists.");

    var user = new UserEntity
    {
        UserName = SanitizeUserName(requestModel.Name),
        Email = requestModel.Email,
        AvatarUrl = "default-avatar.png"
    };

    var result = await userManager.CreateAsync(user, requestModel.Password);

    if (!result.Succeeded)
        return BadRequest(result.Errors);

    var roles = await userManager.GetRolesAsync(user);
    var token = GenerateJwtToken(user, roles);

    var claims = new List<Claim>
    {
        new(ClaimTypes.Email, user.Email),
        new("Avatar", user.AvatarUrl),
        new("Id", user.Id)
    };
    claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    var authProperties = new AuthenticationProperties { IsPersistent = true };

    await HttpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        new(claimsIdentity),
        authProperties);

    logger.LogInformation($"User registered: {requestModel.Name} {requestModel.Email}");
    return Ok(new AuthResponse
    {
        Token = token,
        Email = user.Email,
        Avatar = user.AvatarUrl,
        Roles = roles.ToList(),
        Id = user.Id
    });
}

核心原因分析

  1. 自定义AuthStateProvider未替换默认实现:Blazor默认使用ServerAuthenticationStateProvider(服务器端)或RemoteAuthenticationStateProvider(WASM),若未将自定义WebAuthStateProvider注册为AuthenticationStateProvider的实现,系统会忽略自定义逻辑,默认Provider不会自动感知后端Cookie变化。
  2. 前端未触发状态更新:登录/注册接口成功后,前端未调用自定义Provider的更新方法,导致AuthState保持旧的未认证状态。
  3. Server-Side Blazor的请求级上下文限制:HttpContext是请求级别的,自定义Provider初始化时仅读取一次用户信息,后续认证变化无法自动同步。

解决方案

1. 正确注册自定义Provider

在Program.cs中替换默认的AuthenticationStateProvider:

// 注册自定义Provider为AuthenticationStateProvider的实现
builder.Services.AddScoped<AuthenticationStateProvider, WebAuthStateProvider>();
// 同时注册IAuthProvider接口
builder.Services.AddScoped<IAuthProvider>(sp => sp.GetRequiredService<AuthenticationStateProvider>() as WebAuthStateProvider);

2. 修改前端登录逻辑,触发状态更新

在Login.razor的LoginWithEmail方法中,调用自定义Provider的SaveUser方法同步用户状态:

if (authResponse != null)
{
    var claims = new List<Claim>
    {
        new(ClaimTypes.Email, authResponse.Email),
        new("Avatar", authResponse.Avatar),
        new("Id", authResponse.Id),
        new("JWT", authResponse.Token),
    };
    claims.AddRange(authResponse.Roles.Select(role => new Claim(ClaimTypes.Role, role)));

    // 调用自定义Provider更新用户状态
    await AuthProvider.SaveUser(claims);

    // 此时AuthState已更新
    var test = (await AuthState).User; 

    Navigation.NavigateTo("/feed", true);
}

需在Login.razor中注入依赖:

[Inject] private IAuthProvider AuthProvider { get; set; }
[CascadingParameter] private Task<AuthenticationState> AuthState { get; set; }

3. 优化自定义Provider(Server-Side Blazor)

修改GetAuthenticationStateAsync方法,每次请求读取最新的HttpContext用户信息:

public class WebAuthStateProvider(IHttpContextAccessor httpContextAccessor)
    : AuthenticationStateProvider, IAuthProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
    private ClaimsPrincipal? _cachedUser;

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var user = _cachedUser ?? _httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(new ClaimsIdentity());
        return Task.FromResult(new AuthenticationState(user));
    }

    public async Task SaveUser(IEnumerable<Claim> claims)
    {
        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        _cachedUser = new(identity);
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_cachedUser)));
    }

    public async Task Logout()
    {
        _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_cachedUser)));
    }

    public void NotifyUserAuthentication()
    {
        _cachedUser = _httpContextAccessor.HttpContext?.User;
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}

4. 修正中间件顺序

Program.cs中中间件必须遵循以下顺序:

app.UseRouting();
app.UseAuthentication(); // 必须在UseAuthorization之前
app.UseAuthorization();
// 其他中间件

内容的提问来源于stack exchange,提问作者Rdq

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 14:05:57