Blazor认证自动失效求助:登录1-2分钟后自动登出
我正在实现Blazor认证功能,通过API控制器完成用户登录并返回当前用户状态。目前遇到两个核心问题:
- 用户登录后1.5至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);
内容的

