在当今的Web开发中,JSON Web Token(JWT)已成为身份验证与信息交换的事实标准。然而,许多开发者仅使用JWS(JSON Web Signature)对令牌进行签名,却忽略了JWE(JSON Web Encryption)加密机制,导致敏感数据在传输过程中存在泄露风险。近日,社区中一则关于“如何在.NET中加密JWT,而非仅签名”的技术提问引发广泛关注,折射出开发者对更高级别安全防护的迫切需求。

JWS与JWE:签名与加密的本质差异

JWT有两种主流规范:JWS(签名)和JWE(加密)。JWS通过数字签名确保令牌的完整性和不可否认性,但载荷本身是Base64编码的明文——任何持有令牌的人都可以解码读取内容。而JWE使用加密算法将令牌的载荷加密,只有持有正确密钥的接收方才能解密,从而同时保障数据的机密性与完整性。

在许多场景中,JWS的“明文”特性足以满足身份验证需求:服务器只需校验签名,无需隐藏用户ID、角色等基本信息。然而,当JWT中携带信用卡号、个人隐私、业务敏感字段时,仅签名远远不够。例如,在OAuth2.0的访问令牌中,若包含资源所有者的个人信息,或作为跨系统传递的加密凭证,JWE加密便成为安全底线。

.NET生态中的JWE实现方案

在.NET平台中,实现JWE加密主要有两条路径:一是使用官方推荐的Microsoft.IdentityModel.TokensSystem.IdentityModel.Tokens.Jwt库(建议升级至6.x以上版本);二是借助第三方库如jose-jwtNimbus.JOSE+JWT。微软官方库为.NET开发者提供了最直接的支持,以下通过核心步骤展示实现逻辑。

1. 安装所需NuGet包

dotnet add package Microsoft.IdentityModel.Tokens
dotnet add package System.IdentityModel.Tokens.Jwt

2. 选择加密算法

JWE支持多种加密方案,常见的有: - RSA-OAEP + A128CBC-HS256:非对称加密密钥交换 + 对称内容加密 - ECDH-ES + A128GCM:椭圆曲线密钥协商 + AES-GCM - 直接加密(Direct):直接使用对称密钥加密(需提前安全共享密钥)

对于Web API场景,通常推荐RSA-OAEP方案:使用公钥加密、私钥解密,既避免对称密钥分发难题,又保证性能可控。

3. 加密与生成JWE令牌(使用RSA示例)

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;

var rsaKey = RSA.Create(2048); // 生产环境需妥善保管私钥
var securityKey = new RsaSecurityKey(rsaKey);

var tokenHandler = new JwtSecurityTokenHandler();
var claims = new Dictionary<string, object>
{
    ["sub"] = "user123",
    ["credit_card"] = "4111-1111-1111-1111",
    ["exp"] = DateTime.UtcNow.AddMinutes(30)
};

var encryptionKey = new EncryptedTokenEncryptionKey(securityKey, SecurityAlgorithms.RsaOaepKeyWrap);
var tokenDescriptor = new SecurityTokenDescriptor
{
    Claims = claims,
    SigningCredentials = null, // 可额外签名,但JWE本身已包含完整性校验
    EncryptingCredentials = new EncryptingCredentials(
        securityKey,
        SecurityAlgorithms.RsaOaepKeyWrap,
        SecurityAlgorithms.Aes128CbcHmacSha256)
};

var jweToken = tokenHandler.CreateEncodedJwt(tokenDescriptor);
// 输出结果:eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhDQkMtSFMyNTYifQ...

4. 解密JWE令牌

var tokenValidationParams = new TokenValidationParameters
{
    TokenDecryptionKey = new RsaSecurityKey(rsaKey) { KeyId = "my-key" },
    ValidateIssuer = false,
    ValidateAudience = false,
    ValidateLifetime = true,
    IssuerSigningKey = null // JWE无独立签名时无需设置
};

var principal = tokenHandler.ValidateToken(jweToken, tokenValidationParams, out var validatedToken);
var jwt = validatedToken as JwtSecurityToken;
var creditCard = jwt.Claims.FirstOrDefault(c => c.Type == "credit_card")?.Value;

常见误区与最佳实践

误区一:JWE与JWS必须二选一

实际上,JWE内部可以嵌套JWS,即先签名再加密,或先加密再签名。“签名后加密” 能同时保障数据来源的不可否认性与机密性,适合对安全要求极高的场景(如金融交易)。在.NET中,可通过设置EncryptingCredentials的同时保留SigningCredentials实现。

误区二:密钥管理可以忽视

JWE的安全强度完全取决于密钥的保密性。建议使用Azure Key Vault、AWS KMS等托管密钥服务,或结合ProtectedData类将密钥加密存储在本地配置文件。切勿将私钥硬编码在代码库中。

误区三:加密保护后可以忽略HTTPS

即使JWT被加密,传输层加密(TLS/HTTPS)仍然必要。JWE主要解决端到端的安全性,而TLS防止中间人攻击窃听整个通信链路,两者互为补充。

行业趋势:从签名到加密的演进

随着零信任架构的普及,越来越多的系统倾向对令牌中的敏感字段进行精细化加密。OpenID Connect规范中已明确支持JWE格式的ID Token。在.NET 7/8中,微软进一步优化了JWE的生成性能,并提供了对ECDH-ES等现代化算法的内置支持。开发者在设计API或微服务间通信时,应评估数据敏感度,将JWE作为高安全场景的标配。

结语

“仅签名不加密”的JWT时代已经过去。对于.NET开发者而言,借助成熟库在十分钟内实现JWE加密并非难事。关键是要打破惯性思维,在安全设计阶段就明确“哪些数据必须被加密”,而非等到数据泄露后才追悔莫及。掌握JWE,不仅是对用户数据的尊重,更是专业开发者必须跨越的安全门槛。