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

Blazor Server Cookie认证用户串号问题及注册矛盾求助

问题:Blazor Server Cookie认证串号与AuthenticationStateProvider注册冲突问题

问题现象

  • 首个用户登录后,其他用户打开浏览器会自动以该用户身份完成认证
  • 后续用户登录后,所有在线用户的身份会统一切换为该最新登录用户
  • 用户执行注销操作后,浏览器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生命周期注册,同时通过规范依赖注入方式解决实例为空问题,确保每个用户的认证状态独立隔离。

具体步骤

  1. 修正服务注册生命周期
    将CustomAuthenticationStateProvider改为Scoped注册:

    services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
    
  2. 规范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();
    
  3. 登录/注销后主动刷新认证状态
    在.cshtml的登录/注销逻辑中,完成HttpContext.SignInAsync/SignOutAsync操作后,主动通知认证状态变更:

    // 登录完成后触发状态更新
    var authStateProvider = HttpContext.RequestServices.GetRequiredService<AuthenticationStateProvider>();
    await authStateProvider.NotifyAuthenticationStateChanged(authStateProvider.GetAuthenticationStateAsync());
    
  4. 检查IIS应用程序池配置

    • 启用应用程序池的私有内存限制,避免进程复用导致的状态泄漏
    • 确认应用程序池身份配置正确,无权限问题导致的Cookie读取异常
  5. 验证Cookie隔离配置
    确保Cookie的SameSite和SecurePolicy配置符合生产环境要求,避免跨域或不安全场景下的Cookie共享。

原理说明

  • Blazor Server中每个用户连接对应一个Scoped生命周期,Scoped的AuthenticationStateProvider能确保每个用户的认证状态独立,彻底解决串号问题。
  • 通过主动通知认证状态变更,确保Blazor组件及时获取最新用户身份,避免状态滞后。
  • 禁止Singleton直接依赖Scoped服务,改用Scope按需获取,从根源解决依赖注入时的实例为空问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 09:01:35