.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();
然后生成并应用数据库迁移:
- 打开Package Manager Console,执行:
Add-Migration AddIdentityServerConfigurationStore -Context ApplicationDbContext - 更新数据库:
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处理登录流程,建议保留。
验证步骤
- 部署更新后的服务到AKS
- 获取一个新的JWT Token
- 重启服务
- 使用该旧Token调用API,验证是否能正常通过认证
内容的提问来源于stack exchange,提问作者Aaron

