Blazor Server Cookie认证用户串号问题及注册矛盾求助
问题现象
- 首个用户登录后,其他用户打开浏览器会自动以该用户身份完成认证
- 后续用户登录后,所有在线用户的身份会统一切换为该最新登录用户
- 用户执行注销操作后,浏览器Cookie已删除,但刷新页面会自动以最后登录的用户身份重新登录
排查结论
串号问题的核心原因是CustomAuthenticationStateProvider被注册为Singleton生命周期,改为Scoped生命周期可解决串号问题,但会触发旧问题:依赖注入时CustomAuthenticationStateProvider实例为空。
环境与代码
运行环境
- .NET 6
- Windows Server 2022 IIS生产环境
- 遵循微软官方建议,仅在
.cshtml文件中处理登录/注销逻辑,Blazor组件未使用HttpContextAccessor
服务注册与认证配置
services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IUsersService, UsersService>(); services.AddScoped<IRolesService, RolesService>(); services.AddScoped<ISecurityService, SecurityService>(); services.AddScoped<ICookieValidatorService, CookieValidatorService>(); services.AddScoped<IDbInitializerService, DbInitializerService>(); services.AddSingleton<AuthenticationStateProvider, CustomAuthenticationStateProvider>(); services .AddAuthentication(options => { options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(options => { options.SlidingExpiration = false; options.LoginPath = "/"; options.LogoutPath = "/"; //options.AccessDeniedPath = new PathString("/Home/Forbidden/"); options.Cookie.Name = ".my.app1.cookie"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.SameSite = SameSiteMode.Lax; options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = context => { var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>(); return cookieValidatorService.ValidateAsync(context); } }; });
CustomAuthenticationStateProvider实现代码
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider { private readonly IServiceScopeFactory _scopeFactory; public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory) : base(loggerFactory) => _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30); protected override async Task<bool> ValidateAuthenticationStateAsync( AuthenticationState authenticationState, CancellationToken cancellationToken) { // 新建Scope确保获取最新数据 var scope = _scopeFactory.CreateScope(); try { var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>(); return await ValidateUserAsync(userManager, authenticationState?.User); } finally { if (scope is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync(); } else { scope.Dispose(); } } } private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal) { if (principal is null) { return false; } var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value; if (!int.TryParse(userIdString, out var userId)) { return false; } var user = await userManager.FindUserAsync(userId); return user is not null; } }
解决方案建议
核心思路
保持CustomAuthenticationStateProvider为Scoped生命周期注册,同时通过规范依赖注入方式解决实例为空问题,确保每个用户的认证状态独立隔离。
具体步骤
修正服务注册生命周期
将CustomAuthenticationStateProvider改为Scoped注册:services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();规范Singleton服务的依赖获取方式
若有Singleton生命周期的服务需要访问认证状态,禁止直接注入AuthenticationStateProvider,需通过IServiceScopeFactory在需要时创建Scope,从Scope中获取Scoped的AuthenticationStateProvider实例:// 在Singleton服务中获取认证状态示例 using var scope = _scopeFactory.CreateScope(); var authStateProvider = scope.ServiceProvider.GetRequiredService<AuthenticationStateProvider>(); var authState = await authStateProvider.GetAuthenticationStateAsync();登录/注销后主动刷新认证状态
在.cshtml的登录/注销逻辑中,完成HttpContext.SignInAsync/SignOutAsync操作后,主动通知认证状态变更:// 登录完成后触发状态更新 var authStateProvider = HttpContext.RequestServices.GetRequiredService<AuthenticationStateProvider>(); await authStateProvider.NotifyAuthenticationStateChanged(authStateProvider.GetAuthenticationStateAsync());检查IIS应用程序池配置
- 启用应用程序池的私有内存限制,避免进程复用导致的状态泄漏
- 确认应用程序池身份配置正确,无权限问题导致的Cookie读取异常
验证Cookie隔离配置
确保Cookie的SameSite和SecurePolicy配置符合生产环境要求,避免跨域或不安全场景下的Cookie共享。
原理说明
- Blazor Server中每个用户连接对应一个Scoped生命周期,Scoped的
AuthenticationStateProvider能确保每个用户的认证状态独立,彻底解决串号问题。 - 通过主动通知认证状态变更,确保Blazor组件及时获取最新用户身份,避免状态滞后。
- 禁止Singleton直接依赖Scoped服务,改用Scope按需获取,从根源解决依赖注入时的实例为空问题。
内容的提问来源于stack exchange,提问作者Sadabadi
相关产品推荐
相关产品推荐

