JWT首次验证失败二次成功,疑Microsoft.IdentityModel.Tokens存在问题
JWT首次验证失败、二次验证成功的异常分析与解决
问题现象
C#应用中实现的JWT验证逻辑存在异常:首次调用验证方法时抛出以下异常,第二次调用却能验证成功,无法安全部署到生产环境:
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: 'IDX10503: Signature validation failed. Token does not have a kid. Keys tried: 'System.Text.StringBuilder'.'
验证代码
public static TrustedCredential Validate(string token, string applicationId, string publicKey) { TrustedCredential result = TryToValidate(token, applicationId, publicKey); if (ReferenceEquals(result, null)) { result = TryToValidate(token, applicationId, publicKey); } return result; } private static TrustedCredential TryToValidate(string token, string applicationId, string publicKey) { TokenValidationParameters validationParameters; JwtSecurityTokenHandler handler; ClaimsPrincipal claims; string sub, name, email, tenantId; RSA rsa = null; try { rsa = RSA.Create(); rsa.FromXmlString(publicKey); validationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "codeconecta", ValidateAudience = true, ValidAudience = applicationId, ValidateIssuerSigningKey = true, IssuerSigningKey = new RsaSecurityKey(rsa), ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; handler = new JwtSecurityTokenHandler(); claims = handler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); sub = claims.FindFirst(MicrosoftRegisteredClaimNames.Sub).Value; email = claims.FindFirst(MicrosoftRegisteredClaimNames.Email).Value; tenantId = claims.FindFirst(MicrosoftRegisteredClaimNames.TenantId).Value; name = claims.FindFirst(JwtRegisteredClaimNames.Name).Value; return new TrustedCredential(tenantId, sub, email, name); } catch (SecurityTokenException) { return null; } finally { if (rsa != null) { rsa.Dispose(); rsa = null; } } }
JWT生成代码
SigningCredentials signingCredentials; JwtSecurityToken token; JwtHeader jwtHeader; JwtPayload jwtPayload; using (RSA rsa = RSA.Create()) { rsa.FromXmlString(privateKey); signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256); jwtHeader = new JwtHeader(signingCredentials); jwtHeader["kid"] = "some_kid_value"; jwtPayload = new JwtPayload ( issuer: "codeconecta", audience: connectedApp.id, claims: claims, notBefore: DateTime.UtcNow, expires: DateTime.UtcNow.AddHours(1) ); token = new JwtSecurityToken(jwtHeader, jwtPayload); return (new JwtSecurityTokenHandler()).WriteToken(token); }
自定义Claim类代码
internal class MicrosoftRegisteredClaimNames { internal const string Sub = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; internal const string Email = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; internal const string TenantId = "http://schemas.microsoft.com/identity/claims/tenantid"; }
异常原因
核心问题在于Token的kid(密钥标识)与验证端的密钥KeyId不匹配:
- 生成Token时,手动给JWT头部添加了
kid: "some_kid_value"; - 验证端创建
RsaSecurityKey时,没有设置对应的KeyId属性; Microsoft.IdentityModel.Tokens组件在首次验证时,会严格匹配Token的kid与本地密钥的KeyId,因匹配失败抛出异常;- 二次验证时,组件内部可能存在缓存或 fallback 逻辑,跳过了
kid匹配直接验证签名,因此验证成功。
解决方案
方案1:给验证用密钥设置匹配的KeyId(推荐)
修改验证代码中RsaSecurityKey的创建逻辑,添加与生成Token时一致的KeyId:
validationParameters = new TokenValidationParameters { // 其他参数保持不变 IssuerSigningKey = new RsaSecurityKey(rsa) { KeyId = "some_kid_value" }, };
这样首次验证时组件就能匹配到对应的密钥,直接验证成功,无需二次重试。
方案2:规范Token生成时的kid设置方式
生成Token时,直接给RsaSecurityKey设置KeyId,无需手动修改JwtHeader,避免手动操作导致的不一致:
using (RSA rsa = RSA.Create()) { rsa.FromXmlString(privateKey); var rsaKey = new RsaSecurityKey(rsa); rsaKey.KeyId = "some_kid_value"; // 设置KeyId signingCredentials = new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256); // 无需手动设置jwtHeader["kid"],头部会自动从密钥的KeyId获取 jwtHeader = new JwtHeader(signingCredentials); // 后续payload和token生成逻辑不变 }
方案3:关闭kid验证(不推荐,降低安全性)
如果业务场景不需要通过kid区分密钥,可以在验证参数中设置忽略kid匹配:
validationParameters = new TokenValidationParameters { // 其他参数保持不变 KidValidator = (tokenKid, securityToken, validationParameters) => true };
该方式会跳过kid校验,可能带来安全风险,仅在单一密钥场景下临时使用。
内容的提问来源于stack exchange,提问作者Bruno Leitão
相关产品推荐
相关产品推荐

