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

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不匹配:

  1. 生成Token时,手动给JWT头部添加了kid: "some_kid_value";
  2. 验证端创建RsaSecurityKey时,没有设置对应的KeyId属性;
  3. Microsoft.IdentityModel.Tokens组件在首次验证时,会严格匹配Token的kid与本地密钥的KeyId,因匹配失败抛出异常;
  4. 二次验证时,组件内部可能存在缓存或 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 10:27:55