.NET Core 3.1升级至.NET 8后JWT令牌Kid验证失败求助
近期将项目从.NET Core 3.1升级至.NET 8后,JWT Bearer令牌的验证环节出现失败,具体报错发生在令牌刷新(SlideToken)流程中的验证步骤。
原正常运行代码
Startup认证配置
var appSettings = Configuration.GetSection("AuthSettings").Get<AuthSettings>(); var key = Encoding.ASCII.GetBytes(appSettings.S); services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x => { x.RequireHttpsMetadata = false; x.SaveToken = true; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true }; });
TokenHandler实现
using System; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; using Acme.Services.Api.Code.Settings; using Acme.Services.Core; using Acme.Services.Core.Repository; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace Acme.Services.Api.Controllers { public interface ITokenHandler { string GenerateToken(string[] roles, int userId, int daysOffset); string SlideToken(string replace, out DateTime expires); } public class TokenHandler : ITokenHandler { private readonly ISettingsRepository _settingsRepository; private readonly AuthSettings _authSettings; public TokenHandler(IOptions<AuthSettings> authSettings, ISettingsRepository settingsRepository) { _settingsRepository = settingsRepository; _authSettings = authSettings.Value; } public string GenerateToken(string[] roles, int userId, int daysOffset) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_authSettings.S); var claims = roles.Select(x => new Claim(ClaimTypes.Role, x)).ToList(); claims.Add(new Claim(ClaimTypes.Name, userId.ToString())); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = GetExpiryDateMinutes().AddDays(daysOffset), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } public string SlideToken(string tokenValue, out DateTime expires) { var key = Encoding.ASCII.GetBytes(_authSettings.S); var handler = new JwtSecurityTokenHandler(); var validations = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false }; ClaimsPrincipal claims = handler.ValidateToken(tokenValue, validations, out var tokenSecure); expires = GetExpiryDateMinutes(); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims.Claims), Expires = expires, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = handler.CreateToken(tokenDescriptor); return handler.WriteToken(token); } private DateTime GetExpiryDateMinutes() { var date = DateTime.Now.AddMinutes(int.Parse(_settingsRepository.GetByNameAllowNull("TokenExpiryInMinutes")?.Value ?? "30")); return date; } } }
错误信息
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10517: Signature validation failed. The token's kid is missing. Keys tried: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: 'CPMLsTVgBeiAqC-MBrp6XPSJEfX2rM9anuEqZHauImw'. , KeyId: '. Number of keys in TokenValidationParameters: '1'.
Number of keys in Configuration: '0'.Exceptions caught:
'[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
Token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. See https://aka.ms/IDX10503 for details.at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignatureAndIssuerSecurityKey(String token, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateJWS(String token, TokenValidationParameters validationParameters, BaseConfiguration currentConfiguration, SecurityToken& signatureValidatedToken, ExceptionDispatchInfo& exceptionThrown)
--- End of stack trace from previous location ---
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, JwtSecurityToken outerToken, TokenValidationParameters validationParameters, SecurityToken& signatureValidatedToken)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Acme.Services.Api.Controllers.TokenHandler.SlideToken(String tokenValue, DateTime& expires) in /Users/mattowens/dev/app-Acme-api-portal/api/Acme.Services.Api/Controllers/TokenHandler.cs:line 66
at Acme.Services.Api.SlidingSessionMiddleware.<>c__DisplayClass4_0.b__0() in /Users/mattowens/dev/app-Acme-api-portal/api/Acme.Services.Api/SlidingSessionMiddleware.cs:line 30
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.g__ProcessEvents|241_0(HttpProtocol protocol, Stack`1 events)
项目依赖包
<ItemGroup> <PackageReference Include="AspNetCore.RouteAnalyzer" Version="0.5.3" /> <PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="Check.Password.Strength" Version="1.0.3" /> <PackageReference Include="Edi.RouteDebugger" Version="1.9.0" /> <PackageReference Include="Ical.Net" Version="4.3.1" /> <PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.6.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" /> <PackageReference Include="OfficeOpenXml.Core.ExcelPackage" Version="1.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" /> <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="7.2.0" /> <PackageReference Include="System.Data.SqlClient" Version="4.9.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" /> <PackageReference Include="PartialResponse.AspNetCore.Mvc.Formatters.Json" Version="5.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" /> <PackageReference Include="ValueInjecter" Version="3.2.0" /> </ItemGroup>
错误发生代码行
ClaimsPrincipal claims = handler.ValidateToken(tokenValue, validations, out var tokenSecure);
问题原因与解决方案
问题原因
升级到.NET 8后,使用的System.IdentityModel.Tokens.Jwt 8.x版本对JWT令牌的验证逻辑更严格:默认要求令牌的头部包含kid(密钥ID)字段,而原代码生成的令牌未设置kid,同时验证时仅配置了单个IssuerSigningKey,导致验证过程中无法匹配密钥,触发IDX10517错误。
解决方案
1. 生成令牌时为签名密钥指定固定KeyId
修改令牌生成逻辑,给对称密钥添加固定的KeyId,确保生成的令牌头部包含kid字段:
修改后的GenerateToken方法片段:
public string GenerateToken(string[] roles, int userId, int daysOffset) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_authSettings.S); var securityKey = new SymmetricSecurityKey(key); // 基于密钥生成唯一KeyId,确保全局一致 securityKey.KeyId = Convert.ToBase64String(System.Security.Cryptography.MD5.Create().ComputeHash(key)); var claims = roles.Select(x => new Claim(ClaimTypes.Role, x)).ToList(); claims.Add(new Claim(ClaimTypes.Name, userId.ToString())); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = GetExpiryDateMinutes().AddDays(daysOffset), SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); }
2. 验证时配置IssuerSigningKeys集合
将原代码中单个IssuerSigningKey替换为IssuerSigningKeys集合,包含带KeyId的密钥:
修改Startup中的认证配置
services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x => { x.RequireHttpsMetadata = false; x.SaveToken = true; var key = Encoding.ASCII.GetBytes(appSettings.S); var securityKey = new SymmetricSecurityKey(key); securityKey.KeyId = Convert.ToBase64String(System.Security.Cryptography.MD5.Create().ComputeHash(key)); x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, // 改用IssuerSigningKeys集合 IssuerSigningKeys = new List<SecurityKey> { securityKey }, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true }; });
修改SlideToken中的验证参数
public string SlideToken(string tokenValue, out DateTime expires) { var key = Encoding.ASCII.GetBytes(_authSettings.S); var securityKey = new SymmetricSecurityKey(key); securityKey.KeyId = Convert.ToBase64String(System.Security.Cryptography.MD5.Create().ComputeHash(key)); var handler = new JwtSecurityTokenHandler(); var validations = new TokenValidationParameters { ValidateIssuerSigningKey = true, // 改用IssuerSigningKeys集合 IssuerSigningKeys = new List<SecurityKey> { securityKey }, ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false }; ClaimsPrincipal claims = handler.ValidateToken(tokenValue, validations, out var tokenSecure); expires = GetExpiryDateMinutes(); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims.Claims), Expires = expires, SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature) }; var token = handler.CreateToken(tokenDescriptor); return handler.WriteToken(token); }
3. 可选:临时关闭kid验证(不推荐)
如果暂时无法修改令牌生成逻辑,可通过设置TokenValidationParameters的ValidateIssuerSigningKey为false跳过密钥验证,但这会降低安全性,仅建议作为临时方案:
var validations = new TokenValidationParameters { ValidateIssuerSigningKey = false, // 关闭密钥签名验证 ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false };
内容的提问来源于stack exchange,提问作者KnowHoper

