使用 Microsoft.IdentityModel.JsonWebTokens 创建 JWT 令牌
问题背景
迁移到新的 Microsoft.IdentityModel.JsonWebTokens
库时,许多开发者面临如何将原有基于 System.IdentityModel.Tokens.Jwt
的 JWT 生成代码进行转换的问题。原始代码通常包含以下关键步骤:
- 创建密钥和签名凭证
- 定义声明(claims)集合
- 构建
JwtSecurityToken
对象 - 使用
JwtSecurityTokenHandler
生成令牌字符串
迁移到新库时,主要痛点包括:
- 新库使用不同的类结构(如
JsonWebTokenHandler
代替JwtSecurityTokenHandler
) - 声明(claims)的定义方式发生变化
- 默认时间戳行为的差异需要特别处理
- 缺乏直接等效的
JwtSecurityToken
构造函数
解决方案
以下是将旧版代码迁移到 Microsoft.IdentityModel.JsonWebTokens
的完整实现方案:
安装必需的 NuGet 包
Install-Package Microsoft.IdentityModel.JsonWebTokens
创建令牌的核心代码
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);
关键配置说明
SetDefaultTimesOnTokenCreation=false
禁用此选项可防止处理器自动添加iat
(Issued At) 声明,确保令牌内容与旧实现一致字典声明格式
新库接受Dictionary<string, object>
作为声明集合,相较List<Claim>
更高效显式时间戳管理
需要手动设置NotBefore
和Expires
属性,避免依赖默认行为
新旧实现对比
原始实现生成的 Payload:
{
"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:
{
"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字段值相同
关键优势
迁移到新库的主要好处:
- 性能提升:
JsonWebTokenHandler
相较旧处理器有显著的性能优化 - 灵活性增强:支持更丰富的JSON处理功能
- 符合最新标准:实现OAuth 2.1和OpenID Connect的最新规范
- 声明处理优化:直接使用字典结构简化声明操作
- 更好的大型令牌支持:提升对大尺寸令牌的生成和处理效率
常见迁移陷阱
未禁用默认时间戳
忽略SetDefaultTimesOnTokenCreation=false
会导致自动添加iat
声明错误的声明格式
尝试直接传递List<Claim>
会导致序列化错误未显式设置时间属性
缺少NotBefore/Expires
设置会产生无有效期令牌
进阶场景
自定义声明类型映射
// 示例:简化claim URI的写法
claims["custom:role"] = "Admin";
// 实际生成的声明:
// "custom:role": "Admin"
异步令牌创建(适用于大型令牌)
var tokenString = await handler.CreateTokenAsync(descriptor);
验证生成的结果
var validationResult = handler.ValidateToken(
tokenString,
new TokenValidationParameters
{
ValidIssuer = "MyIssuer",
ValidAudience = "MyAudience",
IssuerSigningKey = securityKey
});
if (!validationResult.IsValid)
{
throw new SecurityTokenException("JWT验证失败");
}