如何每隔一段时间刷新 .net Cookie 标识上的自定义声明?(IE:每半小时一次)

How do I refresh custom claims on a .net Cookie Identity on an interval? (IE: every half hour)

提问人:Jamie Stone 提问时间:11/16/2023 更新时间:11/16/2023 访问量:32

问:

我继承了一个使用 Microsoft Identity 进行身份验证和授权的 ASP.Net 站点。我们使用 SQL 作为身份数据库。

我不理解该网站的声明逻辑。我们正在使用一些自定义声明来指定用户可以访问哪些页面/产品。我希望对其进行设置,以便在用户登录后,当他们导航到新页面时,如果超过 30 分钟,则声明会刷新。我想这样做是为了避免在每个请求上实时检查我们的系统。基本上,在登录时缓存声明 30 分钟,如果超过 30 分钟,请刷新它们。

我不理解身份逻辑。我认为,在SecurityStampValidatorOptions上设置ValidationInterval将使“OnRefreshingPrincipal”在用户通过身份验证后超过半小时被调用。

在本地运行时,此方法永远不会命中。如果自上次刷新声明以来已超过 30 分钟,我应该或可以刷新声明,是否有其他方式?

我的示例(删除了一些重命名和公司特定的详细信息)

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>>();
        }
    }
}

重新验证身份认证状态提供程序.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;
            }
        }
    }

我试图将“ValidationInterval”更改并降低到一分钟,并且没有命中“OnRefreshingPrincipal”方法。

asp.net asp.net-identity 声明基于标识

评论

0赞 Ruikai Feng 11/16/2023
您是否正在使用 blazor?您能否提供一个可以重现您的问题的最小示例?

答:

0赞 Emre Bener 11/16/2023 #1

SignInManager<TUser>.RefreshSignInAsync(TUser)是你要找的方法。此方法将刷新 Cookie 声明。至于在特定的时间间隔内应用它,你可以实现一个最小的中间件。向保留“lastRefreshDate”的 cookie 添加声明,并在中间件中检查自上次刷新以来是否已经过去了 30 分钟。如果是这样,请刷新 cookie 并更新“lastRefreshDate”属性。

public class CookieRefreshMiddleware
{
    private readonly RequestDelegate _next;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public CookieRefreshMiddleware(RequestDelegate next, SignInManager<ApplicationUser> signInManager)
    {
        _next = next;
        _signInManager = signInManager;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var user = context.User;
        if (user.Identity.IsAuthenticated)
        {
            var lastRefreshDateClaim = user.FindFirst("lastRefreshDate");
            if (lastRefreshDateClaim != null)
            {
                var lastRefreshDate = DateTime.Parse(lastRefreshDateClaim.Value);
                if ((DateTime.UtcNow - lastRefreshDate).TotalMinutes >= 30)
                {
                    var appUser = await _signInManager.UserManager.GetUserAsync(user);
                    appUser.Claims.Remove(lastRefreshDateClaim);
                    appUser.Claims.Add(new Claim("lastRefreshDate", DateTime.UtcNow.ToString()));
                    
                    // Update the cookie
                    await _signInManager.RefreshSignInAsync(appUser);
                }
            }
        }

        // Call the next delegate/middleware in the pipeline
        await _next(context);
    }
}

// In Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Other configurations...

    app.UseMiddleware<CookieRefreshMiddleware>();

    // Other configurations...
}

以下是注册中间件的方法。确保将其添加到授权中间件(行)之后。UseAuthorization

app.UseMiddleware<CookieRefreshMiddleware>();

不过,在我看来,您应该在更新用户角色时调用。那么您可能不需要这样的“定期更新”。SignInManager<TUser>.RefreshSignInAsync(TUser)