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

.NET Core 7+Duende IdentityServer6重启后JWT Token失效问题

问题分析与解决方案

核心问题根源

服务重启后旧JWT Token失效,本质是IdentityServer的签名密钥未持久化:默认情况下,IdentityServer会在内存中生成临时签名密钥,服务重启后密钥丢失,旧Token用原密钥签名,新服务用新密钥验证,自然返回invalid_token。另外你的Authentication配置存在冗余,导致验证逻辑冲突。

分步解决方案

1. 移除冗余的JWT Bearer配置

你同时调用了.AddIdentityServerJwt()和.AddJwtBearer(),但.AddIdentityServerJwt()已经自动配置了针对自身IdentityServer的JWT验证(会从IdentityServer的元数据端点获取签名密钥),手动添加的.AddJwtBearer()硬编码了IssuerSigningKey,和IdentityServer实际使用的签名密钥不一致,是导致验证失败的关键原因之一。

删除以下代码块:

.AddJwtBearer(
    options =>
    {
        options.Authority = Configuration["JWT:ValidIssuer"];
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidAudience = Configuration["JWT:ValidAudience"],
            ValidIssuer = Configuration["JWT:ValidIssuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"])),
            NameClaimType = JwtClaimTypes.Name,
            RoleClaimType = JwtClaimTypes.Role
        };
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                StringValues accessToken = context.Request.Query["access_token"];
                PathString path = context.HttpContext.Request.Path;
                if(!string.IsNullOrEmpty(accessToken) &&
                   path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

如果需要支持SignalR的Token读取逻辑,可以合并到.AddIdentityServerJwt()的配置中:

services.AddAuthentication(
        options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
    .AddIdentityServerJwt(options =>
    {
        // 配置SignalR的Token读取逻辑
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                StringValues accessToken = context.Request.Query["access_token"];
                PathString path = context.HttpContext.Request.Path;
                if(!string.IsNullOrEmpty(accessToken) &&
                   path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

2. 持久化IdentityServer的签名密钥

有两种推荐方案,根据你的场景选择:

方案一:数据库存储签名密钥(推荐AKS多实例场景)

你的ApplicationDbContext继承自ApiAuthorizationDbContext,已经内置了配置存储(包含签名密钥)的表结构,只需显式配置并生成迁移:

修改IdentityServer配置代码:

services.AddIdentityServer(
        options =>
        {
            if(_webHostEnvironment.IsProduction())
            {
                options.LicenseKey = Configuration.GetSection("IdentityServer").GetValue<string>("LicenseKey");
            }
        })
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(
        options =>
        {
            options.IdentityResources["openid"].UserClaims.Add(JwtClaimTypes.Name);
            options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Name);
            options.IdentityResources["openid"].UserClaims.Add(JwtClaimTypes.Role);
            options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Role);
        })
    // 配置配置存储(存储客户端、资源、签名密钥)
    .AddConfigurationStore(options =>
    {
        options.ConfigureDbContext = b =>
            b.UseNpgsql(
                Configuration.GetConnectionString("db-connection-string"),
                sql => sql.MigrationsAssembly("API").UseLowerCaseNamingConvention());
    })
    // 配置操作存储(存储授权码、刷新Token等)
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = b =>
            b.UseNpgsql(
                Configuration.GetConnectionString("db-connection-string"),
                sql => sql.MigrationsAssembly("API").UseLowerCaseNamingConvention());
        // 自动清理过期的操作数据(可选)
        options.EnableTokenCleanup = true;
        options.TokenCleanupInterval = 3600; // 每小时清理一次
    })
    // 从配置存储加载签名密钥
    .AddSigningCredentialFromConfigurationStore();

然后生成并应用数据库迁移:

  1. 打开Package Manager Console,执行:
    Add-Migration AddIdentityServerConfigurationStore -Context ApplicationDbContext
    
  2. 更新数据库:
    Update-Database -Context ApplicationDbContext
    

方案二:固定对称签名密钥(适合单实例或测试场景)

如果不想用数据库存储密钥,可以在配置文件中设置一个固定的对称密钥,让IdentityServer始终用该密钥签名Token:

修改IdentityServer配置代码:

services.AddIdentityServer(...)
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(...)
    // 使用固定对称密钥签名
    .AddSigningCredential(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"])));

注意:确保JWT:Secret在所有环境(测试、生产)中完全一致,且妥善保密,不要硬编码到代码中。

3. 关于DataProtection配置的保留

之前的DataProtection配置是用于加密Identity的Cookie(比如登录会话Cookie),如果你还保留了Identity的登录页面或Cookie相关功能,必须保留该配置——它对JWT Token无影响,但能保证Cookie认证的一致性。如果完全不再使用Cookie认证,可以考虑移除,但一般IdentityServer需要Cookie处理登录流程,建议保留。

验证步骤

  1. 部署更新后的服务到AKS
  2. 获取一个新的JWT Token
  3. 重启服务
  4. 使用该旧Token调用API,验证是否能正常通过认证

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 17:45:00