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

Blazor Server动态更新用户权限后认证状态异常问题求助

Blazor Server权限即时生效后认证失败问题解决

问题场景

基于Blazor Server + MudBlazor开发的系统,支持管理员修改普通用户的访问权限(权限存储为用户Claims),要求权限变更即时生效且不中断用户当前操作。目前实现中,调用自定义RevalidatingIdentityAuthenticationStateProvider的RequestReauthentication方法后,用户能看到权限变化,但后续ValidateAuthenticationStateAsync调用ValidateSecurityStampAsync时,获取到的User为null,导致认证状态变为未授权。

问题根源

RequestReauthentication方法构建新ClaimsPrincipal时,仅复制了用户的权限Claims,丢失了SecurityStamp、NameIdentifier等Identity核心标识Claims,导致userManager.GetUserAsync(principal)无法从数据库匹配到对应用户,返回null,触发认证失败逻辑。

修复方案

修改RequestReauthentication方法,构建新身份时保留原Principal中的核心Claims,再合并更新后的权限Claims:

修改后的核心方法代码

public async Task RequestReauthentication(ClaimsPrincipal claimsPrincipal, CancellationToken cancellationToken)
{
    var logger = _loggerFactory.CreateLogger(nameof(RequestReauthentication));
    logger.LogInformation("get auth method called");

    var scope = _scopeFactory.CreateScope();
    try
    {
        var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
        var user = await userManager.GetUserAsync(claimsPrincipal);
        
        if (user is not null && claimsPrincipal.Identity is not null)
        {
            // 获取用户最新权限Claims
            var updatedClaims = await userManager.GetClaimsAsync(user);
            // 保留原Principal中的核心标识Claims
            var coreClaims = claimsPrincipal.Claims
                .Where(c => c.Type == _options.ClaimsIdentity.SecurityStampClaimType 
                        || c.Type == ClaimTypes.NameIdentifier
                        || c.Type == ClaimTypes.Name)
                .ToList();
            
            // 合并核心Claims与更新后的权限Claims(去重,优先保留最新权限)
            var combinedClaims = coreClaims.Concat(updatedClaims)
                .GroupBy(c => c.Type)
                .Select(g => g.Last())
                .ToList();

            // 构建新身份,保留原认证类型
            var newIdentity = new ClaimsIdentity(combinedClaims, claimsPrincipal.Identity.AuthenticationType);
            var newPrincipal = new ClaimsPrincipal(newIdentity);
            
            NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(newPrincipal)));
        }
    }
    catch (Exception ex)
    {
        logger.LogError("exception: {message}", ex.Message);
    }
    finally
    {
        if (scope is IAsyncDisposable asyncDisposable)
        {
            await asyncDisposable.DisposeAsync();
        }
        else
        {
            scope.Dispose();
        }
    }
}

完整修复后类代码

public class RevalidatingIdentityAuthenticationStateProvider<TUser>
        : RevalidatingServerAuthenticationStateProvider where TUser : class
{
    private readonly ILoggerFactory _loggerFactory;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IdentityOptions _options;

    public RevalidatingIdentityAuthenticationStateProvider(
        ILoggerFactory loggerFactory,
        IServiceScopeFactory scopeFactory,
        IOptions<IdentityOptions> optionsAccessor)
        : base(loggerFactory)
    {
        _scopeFactory = scopeFactory;
        _options = optionsAccessor.Value;
        _loggerFactory = loggerFactory;
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromSeconds(10);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        var logger = _loggerFactory.CreateLogger(nameof(ValidateAuthenticationStateAsync));
        var scope = _scopeFactory.CreateScope();
        try
        {
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
            return await ValidateSecurityStampAsync(userManager, authenticationState.User);
        }
        finally
        {
            if (scope is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync();
            }
            else
            {
                scope.Dispose();
            }
        }
    }

    private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
    {
        var logger = _loggerFactory.CreateLogger(nameof(ValidateSecurityStampAsync));
        var user = await userManager.GetUserAsync(principal);
        
        logger.LogInformation("user name test: {test}", principal.Identity.Name);
        
        if (user == null)
        {
            logger.LogInformation("user is null?");
            return false;
        }
        else if (!userManager.SupportsUserSecurityStamp)
        {
            return true;
        }
        else
        {
            var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
            var userStamp = await userManager.GetSecurityStampAsync(user);
            logger.LogInformation("this thing {is}, principal stamp {stamp1}, authtype {type}, user stamp {stamp2}", _options.ClaimsIdentity.SecurityStampClaimType, principalStamp, principal.Identity.AuthenticationType, userStamp);
            
            return principalStamp == userStamp;
        }
    }

    public async Task RequestReauthentication(ClaimsPrincipal claimsPrincipal, CancellationToken cancellationToken)
    {
        var logger = _loggerFactory.CreateLogger(nameof(RequestReauthentication));
        logger.LogInformation("get auth method called");

        var scope = _scopeFactory.CreateScope();
        try
        {
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
            var user = await userManager.GetUserAsync(claimsPrincipal);
            
            if (user is not null && claimsPrincipal.Identity is not null)
            {
                var updatedClaims = await userManager.GetClaimsAsync(user);
                var coreClaims = claimsPrincipal.Claims
                    .Where(c => c.Type == _options.ClaimsIdentity.SecurityStampClaimType 
                            || c.Type == ClaimTypes.NameIdentifier
                            || c.Type == ClaimTypes.Name)
                    .ToList();
                
                var combinedClaims = coreClaims.Concat(updatedClaims)
                    .GroupBy(c => c.Type)
                    .Select(g => g.Last())
                    .ToList();

                var newIdentity = new ClaimsIdentity(combinedClaims, claimsPrincipal.Identity.AuthenticationType);
                var newPrincipal = new ClaimsPrincipal(newIdentity);
                
                NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(newPrincipal)));
            }
        }
        catch (Exception ex)
        {
            logger.LogError("exception: {message}", ex.Message);
        }
        finally
        {
            if (scope is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync();
            }
            else
            {
                scope.Dispose();
            }
        }
    }
}

替代方案(若不想修改认证逻辑)

  • 非关键权限变更:通知用户当前权限将在10分钟后(即RevalidationInterval设置的时长)自动生效,无需登出
  • 关键权限变更:调用SignOutManager强制用户立即登出,重新登录后获取最新权限

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 11:38:10