基于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
相关产品推荐
相关产品推荐

