.NET Cookie Identity自定义声明每30分钟刷新异常问题
我接手了一个采用Microsoft Identity进行身份认证与授权的ASP.NET站点,身份数据库使用SQL。我们通过若干自定义声明控制用户的页面/产品访问权限,希望实现用户登录后,每次导航新页面时若距上次声明刷新超过30分钟则自动刷新,以此避免每次请求实时校验系统。
我原以为配置SecurityStampValidatorOptions的ValidationInterval为30分钟会触发OnRefreshingPrincipal方法,但本地运行时该方法从未被调用。我已尝试缩短ValidationInterval至1分钟,问题仍存在。
相关代码如下:
SiteStartupExtensionMethods.cs
namespace MyWebsite { public static class SiteStartupExtensionMethods { public static void AddMyCustomIdentityAndCookieOptions(this IServiceCollection services, ConfigurationManager configurationManager) { // Add the identity database db context services.AddDbContextFactory<MyIdentityDatabaseDbContext>(options => options.UseSqlServer(configurationManager.GetConnectionString("MyIdentityDatabaseDb")), ServiceLifetime.Transient ); // Add the identity options services.Configure<IdentityOptions>(options => { // Custom options removed for this post }); // Add the identity / entity framework store services.AddIdentity<MyCustomIdentityUser, IdentityRole>() .AddEntityFrameworkStores<MyIdentityDatabaseDbContext>() .AddDefaultTokenProviders() .AddPasswordValidator<CommonPasswordValidator<MyCustomIdentityUser>>() .AddPasswordValidator<UsernameAsPasswordValidator<MyCustomIdentityUser>>(); // Configure the cookie options services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(@"C:\.....")) .SetApplicationName("SharedCookieApp"); services.ConfigureApplicationCookie(options => { options.Cookie.Domain = ".myCustomWebsite.com"; options.Cookie.SameSite = SameSiteMode.None; options.Cookie.HttpOnly = false; options.Cookie.Name = ".AspNet.SharedCookie"; options.LoginPath = new PathString("/login"); options.AccessDeniedPath = new PathString("/access-denied"); options.Events = new CookieAuthenticationEvents() { OnRedirectToLogin = (context) => { // Custom logic removed return Task.CompletedTask; } }; }); // Set the security stamp options services.Configure<SecurityStampValidatorOptions>(options => { options.ValidationInterval = TimeSpan.FromMinutes(30); options.OnRefreshingPrincipal = context => { // This is not being called? // Custom claims refreshing logic was here return Task.FromResult(0); }; }); // Add the claims factory services.AddScoped<IUserClaimsPrincipalFactory<MyCustomIdentityUser>, ClaimsFactory>(); // Add the authentication state provider services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<MyCustomIdentityUser>>(); } } }
RevalidatingIdentityAuthenticationStateProvider.cs
public class RevalidatingIdentityAuthenticationStateProvider<TUser> : RevalidatingServerAuthenticationStateProvider where TUser : class { private readonly IServiceScopeFactory _scopeFactory; private readonly IdentityOptions _options; public RevalidatingIdentityAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory, IOptions<IdentityOptions> optionsAccessor) : base(loggerFactory) { _scopeFactory = scopeFactory; _options = optionsAccessor.Value; } protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); protected override async Task<bool> ValidateAuthenticationStateAsync( AuthenticationState authenticationState, CancellationToken cancellationToken) { // Get the user manager from a new scope to ensure it fetches fresh data 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 user = await userManager.GetUserAsync(principal); if (user == null) { return false; } else if (!userManager.SupportsUserSecurityStamp) { return true; } else { var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType); var userStamp = await userManager.GetSecurityStampAsync(user); return principalStamp == userStamp; } } }
请问是否有其他方法可实现每30分钟刷新自定义声明?
问题核心是你使用的RevalidatingIdentityAuthenticationStateProvider是Blazor Server特有的认证状态验证组件,它会绕过ASP.NET Core Identity默认的SecurityStampValidator逻辑,导致你配置的SecurityStampValidatorOptions无法生效,OnRefreshingPrincipal自然不会被调用。
以下两种方法可以实现需求:
方法一:修改RevalidatingIdentityAuthenticationStateProvider的验证逻辑
在现有验证流程中加入声明刷新判断,当距离上次刷新超过30分钟时,重新生成ClaimsPrincipal并更新认证状态:
- 先在用户Claims中添加记录上次刷新时间的声明,可在
ClaimsFactory的CreateAsync方法中加入:
claims.Add(new Claim("last_claims_refresh", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()));
- 修改
ValidateAuthenticationStateAsync方法:
protected override async Task<bool> ValidateAuthenticationStateAsync( AuthenticationState authenticationState, CancellationToken cancellationToken) { var scope = _scopeFactory.CreateScope(); try { var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>(); var claimsFactory = scope.ServiceProvider.GetRequiredService<IUserClaimsPrincipalFactory<TUser>>(); var user = await userManager.GetUserAsync(authenticationState.User); if (user == null) { return false; } // 验证安全戳 if (userManager.SupportsUserSecurityStamp) { var principalStamp = authenticationState.User.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType); var userStamp = await userManager.GetSecurityStampAsync(user); if (principalStamp != userStamp) { return false; } } // 检查是否需要刷新声明 var lastRefreshClaim = authenticationState.User.FindFirstValue("last_claims_refresh"); if (long.TryParse(lastRefreshClaim, out var lastRefreshTime) && DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeSeconds(lastRefreshTime) > TimeSpan.FromMinutes(30)) { // 重新生成ClaimsPrincipal var newPrincipal = await claimsFactory.CreateAsync(user); // 更新认证状态 NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(newPrincipal))); } return true; } finally { if (scope is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync(); } else { scope.Dispose(); } } }
方法二:禁用自定义认证状态提供者,使用默认SecurityStampValidator
如果不需要Blazor Server特有的实时认证状态重验证,可移除RevalidatingIdentityAuthenticationStateProvider的注册,让默认的SecurityStampValidator生效,此时你配置的OnRefreshingPrincipal会按预期触发:
services.Configure<SecurityStampValidatorOptions>(options => { options.ValidationInterval = TimeSpan.FromMinutes(30); options.OnRefreshingPrincipal = async context => { var userManager = context.Context.RequestServices.GetRequiredService<UserManager<MyCustomIdentityUser>>(); var claimsFactory = context.Context.RequestServices.GetRequiredService<IUserClaimsPrincipalFactory<MyCustomIdentityUser>>(); var user = await userManager.GetUserAsync(context.CurrentPrincipal); if (user != null) { var newPrincipal = await claimsFactory.CreateAsync(user); context.ReplacePrincipal(newPrincipal); } return Task.CompletedTask; }; });
注意:此方法适用于非Blazor Server场景,或Blazor Server中不需要实时重验证认证状态的场景。
内容的提问来源于stack exchange,提问作者Jamie Stone

