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

Blazor认证自动失效求助:登录1-2分钟后自动登出

Blazor认证状态自动失效及Cookie缺失问题排查

我正在实现Blazor认证功能,通过API控制器完成用户登录并返回当前用户状态。目前遇到两个核心问题:

  1. 用户登录后1.5至2分钟内,未主动执行登出操作,认证状态却自动变为未认证;
  2. 浏览器开发者工具的应用面板中找不到名为“IB”的认证Cookie。

已尝试刷新页面排查是否与活动状态相关,但问题依旧。以下是相关代码,请协助排查:


自定义AuthenticationStateProvider代码

using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using Domains.Users;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Implementations.Users;

public interface IAccountService
{
     public Task<LoginResponse> Login(string email, string password);
     public Task<bool> Logout();
}

public class CustomAuthenticationStateProvider : AuthenticationStateProvider,IAccountService
{
    private bool _authenticated = false;

    private readonly ClaimsPrincipal Unauthenticated =
        new(new ClaimsIdentity());
    
    private readonly HttpClient _httpClient;
    private readonly NavigationManager NavigationManager;

    public CustomAuthenticationStateProvider(IHttpClientFactory httpClientFactory,NavigationManager manager)
    {
        _httpClient = httpClientFactory.CreateClient("Auth");
        NavigationManager = manager;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        _authenticated = false;

        var user = Unauthenticated;
        
        try
        {
            var response = await _httpClient.GetAsync($"{NavigationManager.BaseUri}api/CurrentUser");

            if (response.IsSuccessStatusCode && response.StatusCode == HttpStatusCode.OK)
            {
                var currentUser = await response.Content.ReadFromJsonAsync<User>();

                if (currentUser != null)
                {
                    List<Claim> claims = new List<Claim>();
                    claims.Add(new Claim("UserEmail", currentUser.userEmail));
                    claims.Add(new Claim("UserId", currentUser.userId));
                    claims.Add(new Claim("UserStatus", ((int)currentUser.userStatus).ToString()));

                    foreach (var role in currentUser.userRoles)
                    {
                        var claim = new Claim(ClaimTypes.Role, role.roleType);
                        claim.Properties["ClaimId"] = role.roleId;
                        claims.Add(claim);
                    }

                    var claimsIdentity = new ClaimsIdentity(claims, nameof(CustomAuthenticationStateProvider));
                    
                    user = new ClaimsPrincipal(claimsIdentity);
                } 
            }
            else
            {
                Console.WriteLine($"Failed to retrieve user: {response.ReasonPhrase}");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            throw;
        }
        
        return new AuthenticationState(user);
    }

    public async Task<LoginResponse> Login(string email, string password)
    {
        try
        {
            var user = new UserModel()
            {
                userEmail = email,
                userPassword = password,
            };
            
            var response = await _httpClient.PostAsJsonAsync($"{NavigationManager.BaseUri}api/login", user);

            if (response.IsSuccessStatusCode && response.StatusCode == HttpStatusCode.OK)
            {
                NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
            }

            if (response.Content == null) 
                return new LoginResponse(false, "Server is Down");
            
            var responseContent = await response.Content.ReadFromJsonAsync<LoginResponse>();

            return responseContent;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

    public async Task<bool> Logout()
    {
        try
        {
            var response = await _httpClient.PostAsync($"{NavigationManager.BaseUri}api/logout",null);

            if (response.IsSuccessStatusCode)
            {
                NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
                return true;
            }

            return false;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }
}

Login API控制器代码

using System.Security.Claims;
using Domains.Users;
using Implementations.Users;
using Interfaces.Users;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;

namespace Implementations.Controllers;

[Route("/api")]
[ApiController]
public class LoginController:ControllerBase
{
    private readonly IUserService userService;

    public LoginController(IUserService userService)
    {
        this.userService = userService;
    }
    
    [HttpPost]
    [Route("login")]
    public async Task<LoginResponse> Login([FromBody] UserModel user)
    {
        try
        {
           var currentUser = await userService.CheckUserCredentials(user.userEmail,user.userPassword);

            if (currentUser.userStatus == Domains.Users.User.LoginStatus.allowed)
            {
                List<Claim> userClaims = new List<Claim>(new []{
                    new Claim("UserId", currentUser.userId),
                    new Claim("UserEmail", currentUser.userEmail),
                    new Claim("UserStatus", ((int)currentUser.userStatus).ToString())
                });
                
                var userRoles = await userService.GetUserRoles(currentUser.userId);
                
                foreach (var role in userRoles)
                {
                   var claim = new Claim(ClaimTypes.Role, role.roleType);
                   claim.Properties["ClaimId"] = role.roleId;
                   
                   userClaims.Add(claim);
                }
                
                var identity = new ClaimsIdentity(userClaims,CookieAuthenticationDefaults.AuthenticationScheme);
                var principal = new ClaimsPrincipal(identity);

                var props = new AuthenticationProperties();
               
                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,principal, props);

                var response = new LoginResponse(true, "login successful");

                return response;
            }
            
            if (currentUser.userStatus == Domains.Users.User.LoginStatus.blocked)
            {
                return new LoginResponse(false, "user blocked");;
            }
            else if (currentUser.userStatus == Domains.Users.User.LoginStatus.passwordWrong)
            {
                return new LoginResponse(false, "verify password or username ");
            }
        }
        catch (Exception e)
        {
            return new LoginResponse(false, "issue while login");
        }

        return new LoginResponse(false,"issue while login");
    }
    
    [HttpPost]
    [Route("logout")]
    public async Task<IActionResult> Logout()
    {
        if (HttpContext.User.Identity.IsAuthenticated)
        {
            await HttpContext.SignOutAsync();
            return Ok();
        }

        return BadRequest("No user is currently logged in");
    }
    
    [HttpGet]
    [Route("CurrentUser")]
    public User GetCurrentUser()
    {
        if (HttpContext == null || HttpContext.User == null) 
            return null;
            
        if (HttpContext.User.Identity.IsAuthenticated)
        {
           var authCookie = HttpContext.Request.Cookies["IB"];

           var user= HttpContext.User;
           var claims= user.Claims.Where(claim => claim.Type.Equals(ClaimTypes.Role));

           List<UserRoles> userRoles = new();
           
           foreach (var claim in claims)
           {
               userRoles.Add(new UserRoles()
               {
                   roleId = claim.Properties["ClaimId"],
                   roleType = claim.Value
               });
           }  
           
           return new User()
           {
               userEmail = user.Claims.FirstOrDefault(claim => claim.Type.Equals("UserEmail")).Value,
               userId = user.Claims.FirstOrDefault(claim => claim.Type.Equals("UserId")).Value,
               userStatus =((Domains.Users.User.LoginStatus)
                   int.Parse(user.Claims.FirstOrDefault(claim => claim.Type.Equals("UserStatus")).Value)),
               userRoles = userRoles
           };
        }
        return null;
    }
}

Program.cs代码

using Blazored.LocalStorage;
using Blazored.SessionStorage;
using Implementations.CartService;
using Implementations.Users;
using Infrastructure.Users;
using Interfaces.Users;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using UI.Components.DialogBox;
using UI.Pages.Home;
using WebApp.Components;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
builder.Services.AddScoped(sp => (IAccountService)sp.GetRequiredService<AuthenticationStateProvider>());

builder.Services.AddHttpClient();
builder.Services.AddControllers();

builder.Services.AddBlazoredLocalStorage();
builder.Services.AddHttpContextAccessor();
builder.Services.AddBlazoredSessionStorage();

builder.Services.AddScoped<IUserStorage,UserStorage>();
builder.Services.AddScoped<IUserService,UserService>();
builder.Services.AddScoped<CartService>();
builder.Services.AddSingleton<DialogService>();

builder.Services.AddAuthorization();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).
    AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,options =>
{
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan=TimeSpan.FromHours(10); 
    options.SlidingExpiration = false;
    options.Cookie.Name = "IB";

    options.Cookie.MaxAge=TimeSpan.FromDays(2);
} );

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.MapControllerRoute("Login", "api/Login");
app.MapControllerRoute("CurrentUser", "api/CurrentUser");
app.UseStaticFiles();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.UseCookiePolicy();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode()
    .AddAdditionalAssemblies(typeof(HomePage).Assembly).AddInteractiveServerRenderMode();

app.Run();

问题排查与修复建议

1. Cookie未显示及无法持久化的核心原因

(1)HttpClient未配置Cookie容器

自定义AuthenticationStateProvider中使用的"Auth"命名HttpClient没有配置Cookie容器,导致登录接口返回的Cookie无法被保存,后续请求/api/CurrentUser时无法携带认证Cookie,接口返回未认证状态。
修复:在Program.cs中配置命名HttpClient时添加Cookie容器:

builder.Services.AddHttpClient("Auth", client =>
{
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    UseCookies = true,
    CookieContainer = new CookieContainer()
});

(2)Cookie属性冲突且未标记持久化

Program.cs中同时设置ExpireTimeSpan=TimeSpan.FromHours(10)和Cookie.MaxAge=TimeSpan.FromDays(2),两者优先级冲突(MaxAge会覆盖ExpireTimeSpan),且未设置IsPersistent=true,导致Cookie默认是会话型(浏览器关闭即失效),甚至可能被浏览器拦截。
修复:统一Cookie过期配置,明确持久化:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.Name = "IB";
        options.ExpireTimeSpan = TimeSpan.FromHours(10);
        options.SlidingExpiration = true; // 用户活跃时自动延长有效期
        options.Cookie.IsPersistent = true; // 标记为持久Cookie
        options.Cookie.MaxAge = options.ExpireTimeSpan; // 保持MaxAge与过期时间一致
    });

(3)登录接口未配置持久化属性

Login控制器中调用SignInAsync时,AuthenticationProperties未设置IsPersistent=true,导致生成的Cookie是会话型,无法长期保存。
修复:登录时添加持久化配置:

var props = new AuthenticationProperties
{
    IsPersistent = true,
    ExpiresUtc = DateTimeOffset.UtcNow.AddHours(10),
    AllowRefresh = true
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props);

2. 认证状态自动失效的原因

(1)GetCurrentUser接口冗余Cookie检查

Login控制器的GetCurrentUser方法中手动检查HttpContext.Request.Cookies["IB"],但实际认证状态应通过HttpContext.User.Identity.IsAuthenticated判断,若Cookie名称有误或未携带,会导致返回null,触发AuthenticationStateProvider返回未认证状态。
修复:移除冗余的Cookie检查,直接依赖Claims:

[HttpGet]
[Route("CurrentUser")]
public User GetCurrentUser()
{
    if (HttpContext?.User == null || !HttpContext.User.Identity.IsAuthenticated)
        return null;
    
    var user = HttpContext.User;
    var roleClaims = user.Claims.Where(c => c.Type == ClaimTypes.Role);
    
    List<UserRoles> userRoles = roleClaims.Select(c => new UserRoles
    {
        roleId = c.Properties["ClaimId"],
        roleType = c.Value
    }).ToList();
    
    return new User
    {
        userEmail = user.FindFirst("UserEmail")?.Value,
        userId = user.FindFirst("UserId")?.Value,
        userStatus = (Domains.Users.User.LoginStatus)int.Parse(user.FindFirst("UserStatus")?.Value ?? "0"),
        userRoles = userRoles
    };
}

(2)AuthenticationStateProvider未缓存状态

当前GetAuthenticationStateAsync每次都调用API接口,若接口因Cookie问题返回未认证,会立即更新状态。添加本地缓存可减少不必要的API调用,同时在登录/登出时手动更新缓存。
修复:修改CustomAuthenticationStateProvider添加缓存逻辑:

private ClaimsPrincipal _cachedUser;

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
    if (_cachedUser != null)
        return new AuthenticationState(_cachedUser);

    var user = Unauthenticated;
    try
    {
        var response = await _httpClient.GetAsync("api/CurrentUser");
        if (response.IsSuccessStatusCode)
        {
            var currentUser = await response.Content.ReadFromJsonAsync<User>();
            if (currentUser != null)
            {
                var claims = new List<Claim>
                {
                    new Claim("UserEmail", currentUser.userEmail),
                    new Claim("UserId", currentUser.userId),
                    new Claim("UserStatus", ((int)currentUser.userStatus).ToString())
                };
                claims.AddRange(currentUser.userRoles.Select(r => 
                    new Claim(ClaimTypes.Role, r.roleType) { Properties = { ["ClaimId"] = r.roleId } }));
                
                var identity = new ClaimsIdentity(claims, nameof(CustomAuthenticationStateProvider));
                user = new ClaimsPrincipal(identity);
                _cachedUser = user;
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
    return new AuthenticationState(user);
}

public async Task<LoginResponse> Login(string email, string password)
{
    try
    {
        var userModel = new UserModel { userEmail = email, userPassword = password };
        var response = await _httpClient.PostAsJsonAsync("api/login", userModel);
        if (response.IsSuccessStatusCode)
        {
            _cachedUser = null; // 清除缓存,强制重新获取状态
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }
        return response.Content == null ? new LoginResponse(false, "Server is Down") 
            : await response.Content.ReadFromJsonAsync<LoginResponse>();
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

public async Task<bool> Logout()
{
    try
    {
        var response = await _httpClient.PostAsync("api/logout", null);
        if (response.IsSuccessStatusCode)
        {
            _cachedUser = Unauthenticated;
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
            return true;
        }
        return false;
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
        throw;
    }
}

3. 其他配置问题

(1)路由配置冗余

已调用app.MapControllers(),无需额外添加app.MapControllerRoute("Login", "api/Login")和app.MapControllerRoute("CurrentUser", "api/CurrentUser"),重复路由可能导致请求处理异常,直接删除即可。

(2)中间件顺序错误

app.UseCookiePolicy()应放在app.UseAuthentication()之前,确保Cookie规则在认证前生效。调整后的中间件顺序:

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode()
    .AddAdditionalAssemblies(typeof(HomePage).Assembly);

内容的

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 00:17:08