Skip to content

使用 Microsoft.IdentityModel.JsonWebTokens 创建 JWT 令牌

问题背景

迁移到新的 Microsoft.IdentityModel.JsonWebTokens 库时,许多开发者面临如何将原有基于 System.IdentityModel.Tokens.Jwt 的 JWT 生成代码进行转换的问题。原始代码通常包含以下关键步骤:

  1. 创建密钥和签名凭证
  2. 定义声明(claims)集合
  3. 构建 JwtSecurityToken 对象
  4. 使用 JwtSecurityTokenHandler 生成令牌字符串

迁移到新库时,主要痛点包括:

  • 新库使用不同的类结构(如 JsonWebTokenHandler 代替 JwtSecurityTokenHandler
  • 声明(claims)的定义方式发生变化
  • 默认时间戳行为的差异需要特别处理
  • 缺乏直接等效的 JwtSecurityToken 构造函数

解决方案

以下是将旧版代码迁移到 Microsoft.IdentityModel.JsonWebTokens 的完整实现方案:

安装必需的 NuGet 包

bash
Install-Package Microsoft.IdentityModel.JsonWebTokens

创建令牌的核心代码

csharp
using Microsoft.IdentityModel.Tokens;
using System.Text;

// 生成安全密钥
var secret = "SomeStringFromConfig1234 SomeStringFromConfig1234";
var data = Encoding.UTF8.GetBytes(secret);
var securityKey = new SymmetricSecurityKey(data);

// 创建声明集合(使用字典代替List<Claim>)
var claims = new Dictionary<string, object>
{
    [ClaimTypes.Name] = "Testuser",
    [ClaimTypes.GroupSid] = "Tenant1",
    [ClaimTypes.Sid] = "3c545f1c-cc1b-4cd5-985b-8666886f985b"
};

// 配置令牌描述器
var descriptor = new SecurityTokenDescriptor
{
    Issuer = "MyIssuer",
    Audience = "MyAudience",
    Claims = claims, // 直接使用字典声明
    IssuedAt = null, // 显式禁止IssuedAt自动生成
    NotBefore = DateTime.UtcNow,
    Expires = DateTime.UtcNow.AddMinutes(120),
    SigningCredentials = new SigningCredentials(
        securityKey, 
        SecurityAlgorithms.HmacSha256Signature
    )
};

// 创建并配置令牌处理器
var handler = new JsonWebTokenHandler();
handler.SetDefaultTimesOnTokenCreation = false; // 关键:禁用默认时间戳

// 生成JWT字符串
var tokenString = handler.CreateToken(descriptor);

关键配置说明

  1. SetDefaultTimesOnTokenCreation=false
    禁用此选项可防止处理器自动添加 iat (Issued At) 声明,确保令牌内容与旧实现一致

  2. 字典声明格式
    新库接受 Dictionary<string, object> 作为声明集合,相较 List<Claim> 更高效

  3. 显式时间戳管理
    需要手动设置 NotBeforeExpires 属性,避免依赖默认行为

新旧实现对比

原始实现生成的 Payload:

json
{
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Testuser",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid": "Tenant1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid": "3c545f1c-cc1b-4cd5-985b-8666886f985b",
  "nbf": 1708992000,
  "exp": 1709078400,
  "iss": "MyIssuer",
  "aud": "MyAudience"
}

新实现产生的 Payload:

json
{
  "aud": "MyAudience",
  "iss": "MyIssuer",
  "exp": 1709078400,
  "nbf": 1708992000,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Testuser",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid": "Tenant1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid": "3c545f1c-cc1b-4cd5-985b-8666886f985b"
}

重要差异说明

虽然字段顺序不同(JWT规范认为字段顺序无关紧要),但实际声明内容完全一致:

  • 两者包含相同的声明类型和值
  • 时间戳(nbf, exp)数值一致
  • Issuer和Audience字段值相同

关键优势

迁移到新库的主要好处:

  1. 性能提升JsonWebTokenHandler 相较旧处理器有显著的性能优化
  2. 灵活性增强:支持更丰富的JSON处理功能
  3. 符合最新标准:实现OAuth 2.1和OpenID Connect的最新规范
  4. 声明处理优化:直接使用字典结构简化声明操作
  5. 更好的大型令牌支持:提升对大尺寸令牌的生成和处理效率

常见迁移陷阱

  1. 未禁用默认时间戳
    忽略 SetDefaultTimesOnTokenCreation=false 会导致自动添加 iat 声明

  2. 错误的声明格式
    尝试直接传递 List<Claim> 会导致序列化错误

  3. 未显式设置时间属性
    缺少 NotBefore/Expires 设置会产生无有效期令牌

进阶场景

自定义声明类型映射

csharp
// 示例:简化claim URI的写法
claims["custom:role"] = "Admin";

// 实际生成的声明:
// "custom:role": "Admin"

异步令牌创建(适用于大型令牌)

csharp
var tokenString = await handler.CreateTokenAsync(descriptor);

验证生成的结果

csharp
var validationResult = handler.ValidateToken(
    tokenString, 
    new TokenValidationParameters
    {
        ValidIssuer = "MyIssuer",
        ValidAudience = "MyAudience",
        IssuerSigningKey = securityKey
    });

if (!validationResult.IsValid)
{
    throw new SecurityTokenException("JWT验证失败");
}