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
相关产品推荐
相关产品推荐

