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

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

关键说明

  1. 不要手动修改Cookie:context.ShouldRenew = true会告诉认证中间件重新生成Cookie,自动使用新的会话密钥更新Cookie值,确保Cookie的所有属性(过期时间、HttpOnly、SameSite等)与配置一致。
  2. 密钥生成逻辑:使用RandomNumberGenerator生成加密安全的随机数,避免使用Guid或DateTime这类可预测的数值,提升安全性。
  3. 缓存键前缀:给缓存键添加前缀,避免与其他缓存数据冲突,也方便后续扩展(比如改用分布式缓存时的键管理)。
  4. RefreshSignInAsync的局限性:该方法默认仅刷新票据的过期时间,不会生成新的会话密钥,必须通过OnValidatePrincipal结合TicketStore逻辑实现密钥轮换。

内容的提问来源于stack exchange,提问作者jacek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 20:52:02