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

基于JWT与Microsoft.IdentityModel.Tokens的JWE加密验证解密问题

解决方案:.NET 8中正确生成带签名与加密的JWE并传递自定义负载

核心问题拆解与修正思路

1. CreateToken方法选择

无需纠结字符串负载+SecurityTokenDescriptor的组合重载,正确方式是使用SecurityTokenDescriptor统一配置所有内容:包括标准验证声明(iss/aud/exp等)、自定义负载、签名与加密凭证。该方式能自动处理标准声明的生成与序列化,避免手动拼接的错误。

2. 生命周期验证失败问题

直接传入字符串负载的CreateToken重载仅将字符串原样作为Payload,不会自动添加exp/nbf等标准生命周期声明,即使设置handler的TokenLifetimeInMinutes也无效。必须通过SecurityTokenDescriptor的Expires/NotBefore字段配置,才能生成合法的生命周期声明,通过验证。

3. 自定义负载放置位置

自定义业务负载应放入SecurityTokenDescriptor的Claims集合,而非InnerHeaderClaims。Header仅用于存放JWT元数据(如加密算法、密钥ID),Payload才是业务数据的合法区域。


完整实现代码

1. 密钥初始化(保留原逻辑,优化格式)

string rsaKeys;
using (var streamReader = File.OpenText(@"rsa-crypt.pem"))
    rsaKeys = streamReader.ReadToEnd();

string eccSigningKey;
using (var streamReader = File.OpenText(@"ecc-crypt.pem"))
    eccSigningKey = streamReader.ReadToEnd();

// 加密密钥(RSA)
var privateEncKeyId = "somerandomid1234";
var privateKey = RSA.Create();
privateKey.ImportFromPem(rsaKeys.ToCharArray());
var privateEncryptionKey = new RsaSecurityKey(privateKey) { KeyId = privateEncKeyId };
var publicEncryptionKey = new RsaSecurityKey(privateKey.ExportParameters(false)) { KeyId = privateEncKeyId };

// 签名密钥(ECDSA)
var privateSigningKeyId = "somerandomid5678";
var privateSignKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
privateSignKey.ImportFromPem(eccSigningKey.ToCharArray());
var privateSigningKey = new ECDsaSecurityKey(privateSignKey) { KeyId = privateSigningKeyId };
var publicSigningKey = new ECDsaSecurityKey(ECDsa.Create(privateSignKey.ExportParameters(false))) { KeyId = privateSigningKeyId };

// 凭证配置
var signingCredentials = new SigningCredentials(privateSigningKey, SecurityAlgorithms.EcdsaSha256);
var encryptingCredentials = new EncryptingCredentials(publicEncryptionKey, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512);

// 自定义序列化负载
var mySerializedClassPayload = "{\"id\":1,\"phone\":\"1112223333\"}";

2. 正确生成JWE(包含标准验证声明与自定义负载)

var descriptor = new SecurityTokenDescriptor
{
    // 标准验证声明
    Issuer = "test-issuer",
    Audience = "https://test.com",
    Expires = DateTime.UtcNow.AddMinutes(1),
    NotBefore = DateTime.UtcNow,
    IssuedAt = DateTime.UtcNow,

    // 自定义负载:放入Claims集合(合法的Payload区域)
    Claims = new Dictionary<string, object>
    {
        {"custom_payload", mySerializedClassPayload}
        // 也可拆分JSON字段为单独Claim:{"id", 1}, {"phone", "1112223333"}
    },

    // 元数据Header(可选)
    AdditionalHeaderClaims = new Dictionary<string, object>
    {
        {"test-outer-header-claim", "abc123456"}
    },

    // 签名与加密凭证
    SigningCredentials = signingCredentials,
    EncryptingCredentials = encryptingCredentials
};

var handler = new JsonWebTokenHandler();
var validJweToken = handler.CreateToken(descriptor);

3. 接收方验证与解密JWE

// 验证配置
var validationParameters = new TokenValidationParameters
{
    ValidIssuer = "test-issuer",
    ValidAudience = "https://test.com",
    IssuerSigningKey = publicSigningKey, // 用于验证签名
    TokenDecryptionKey = privateEncryptionKey, // 用于解密JWE
    ValidateLifetime = true,
    ValidateIssuer = true,
    ValidateAudience = true
};

// 验证并解密
var result = handler.ValidateToken(validJweToken, validationParameters);
if (result.IsValid)
{
    // 获取自定义负载
    var customPayload = result.Claims.First(c => c.Type == "custom_payload").Value;
    // 反序列化为业务对象:var myClass = JsonSerializer.Deserialize<MyClass>(customPayload);
}

关键说明

  • JWE结构:上述代码生成的是嵌套JWT(先签名生成JWS,再加密生成JWE),既保证了负载的机密性(加密),又保证了数据的完整性与不可否认性(签名)。
  • 自定义负载处理:若自定义负载是复杂对象,建议直接将对象的属性拆分为单独的Claim,而非存储整个JSON字符串,这样接收方无需额外反序列化即可直接获取字段值。
  • 生产环境注意:需使用合法的CA证书生成密钥,避免使用自签密钥用于生产环境;密钥需妥善保管,避免泄露。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 16:04:50