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

.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并更新认证状态:

  1. 先在用户Claims中添加记录上次刷新时间的声明,可在ClaimsFactory的CreateAsync方法中加入:
claims.Add(new Claim("last_claims_refresh", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()));
  1. 修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 23:35:58