ASP.NET Core Identity:RefreshSignInAsync不生成新Cookie票据密钥的解决
解决方案
核心问题分析
直接手动修改Cookie值的方式不可行,因为ASP.NET Core的Cookie认证中间件对Cookie的生成和验证有严格的流程控制:当使用SessionStore时,Cookie中存储的是会话密钥,而非加密的认证票据,这个密钥需要和服务器缓存中的票据一一对应,且必须通过认证中间件的内置机制更新,才能被系统识别。
步骤1:优化MemoryCacheTicketStore
首先调整MemoryCacheTicketStore,使用框架注入的IMemoryCache而非自行实例化,同时统一密钥生成逻辑(使用加密安全的随机数):
using Microsoft.Extensions.Caching.Memory; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; public class MemoryCacheTicketStore : ITicketStore { private readonly IMemoryCache _cache; private const string CacheKeyPrefix = "TicketStore:"; // 注入框架提供的IMemoryCache,避免自行管理缓存实例 public MemoryCacheTicketStore(IMemoryCache cache) { _cache = cache; } public Task ClearAll() { // 内存缓存不支持批量遍历删除,若需批量清理建议改用IDistributedCache // 临时方案:重建缓存实例(仅用于开发测试,生产环境不推荐) var oldCache = _cache; _cache = new MemoryCache(new MemoryCacheOptions()); oldCache.Dispose(); return Task.CompletedTask; } public async Task<string> StoreAsync(AuthenticationTicket ticket) { // 使用加密安全的随机数生成密钥,比DateTime+Guid更安全 var key = GenerateSecureKey(); await RenewAsync(key, ticket); return key; } public Task RenewAsync(string key, AuthenticationTicket ticket) { var options = new MemoryCacheEntryOptions(); var expiresUtc = ticket.Properties.ExpiresUtc; if (expiresUtc.HasValue) { options.SetAbsoluteExpiration(expiresUtc.Value); } // 滑动过期时间与Cookie配置保持一致 options.SetSlidingExpiration(TimeSpan.FromDays(36500)); _cache.Set($"{CacheKeyPrefix}{key}", ticket, options); return Task.CompletedTask; } public Task<AuthenticationTicket> RetrieveAsync(string key) { _cache.TryGetValue($"{CacheKeyPrefix}{key}", out AuthenticationTicket ticket); return Task.FromResult(ticket); } public Task RemoveAsync(string key) { _cache.Remove($"{CacheKeyPrefix}{key}"); return Task.CompletedTask; } // 生成符合安全标准的会话密钥 public string GenerateSecureKey() { using var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); byte[] keyBytes = new byte[32]; // 256位密钥,足够安全 rng.GetBytes(keyBytes); return Convert.ToBase64String(keyBytes); } }
步骤2:修改Program.cs的认证配置
通过OnValidatePrincipal事件实现密钥轮换逻辑,让认证中间件自动处理Cookie更新:
builder.Services.AddDbContext<AppDbContext>(options => options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); builder.Services.AddIdentity<User, IdentityRole>() .AddEntityFrameworkStores<AppDbContext>(); // 注册自定义TicketStore,注入IMemoryCache builder.Services.AddSingleton<ITicketStore, MemoryCacheTicketStore>(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); builder.Services.ConfigureApplicationCookie(options => { options.ExpireTimeSpan = TimeSpan.FromDays(36500); options.SlidingExpiration = true; // 从服务容器获取注册的TicketStore options.SessionStore = builder.Services.BuildServiceProvider().GetRequiredService<ITicketStore>(); options.Cookie.Name = "identity_token"; options.Events.OnValidatePrincipal = async context => { // 获取当前Cookie中的旧会话密钥 var oldKey = context.Request.Cookies[options.Cookie.Name]; if (string.IsNullOrEmpty(oldKey)) { return; } // 从TicketStore获取旧票据 var ticketStore = context.HttpContext.RequestServices.GetRequiredService<ITicketStore>() as MemoryCacheTicketStore; var oldTicket = await ticketStore.RetrieveAsync(oldKey); if (oldTicket == null) { // 旧票据不存在,拒绝认证 context.RejectPrincipal(); return; } // 生成新的会话密钥 var newKey = ticketStore.GenerateSecureKey(); // 将旧票据存入新密钥对应的缓存 await ticketStore.RenewAsync(newKey, oldTicket); // 删除旧密钥的缓存 await ticketStore.RemoveAsync(oldKey); // 更新票据的过期时间 oldTicket.Properties.IssuedUtc = DateTimeOffset.UtcNow; oldTicket.Properties.ExpiresUtc = DateTimeOffset.UtcNow.Add(options.ExpireTimeSpan); // 替换Principal并标记需要更新Cookie context.ReplacePrincipal(oldTicket.Principal); context.ShouldRenew = true; // 无需手动写入Cookie,context.ShouldRenew会触发中间件自动更新Cookie await Task.CompletedTask; }; });
关键说明
- 不要手动修改Cookie:
context.ShouldRenew = true会告诉认证中间件重新生成Cookie,自动使用新的会话密钥更新Cookie值,确保Cookie的所有属性(过期时间、HttpOnly、SameSite等)与配置一致。 - 密钥生成逻辑:使用
RandomNumberGenerator生成加密安全的随机数,避免使用Guid或DateTime这类可预测的数值,提升安全性。 - 缓存键前缀:给缓存键添加前缀,避免与其他缓存数据冲突,也方便后续扩展(比如改用分布式缓存时的键管理)。
- RefreshSignInAsync的局限性:该方法默认仅刷新票据的过期时间,不会生成新的会话密钥,必须通过
OnValidatePrincipal结合TicketStore逻辑实现密钥轮换。
内容的提问来源于stack exchange,提问作者jacek
相关产品推荐
相关产品推荐

