.net9 解析 jwt 详解
JWT 解析
- 载荷 payload 详解
- JWT标准字段
- Keycloak 特有字段
- 用户信息字段
- 请求头解析 token
- 如何使用 .net 解析完整的 jwt
- 解析 jwt 验证
- 总结
这是一个 Keycloak 26.2
签发的 JWT (JSON Web Token)
载荷部分(payload
)的内容:
{"exp": 1755849788,"iat": 1755847988,"auth_time": 1755847988,"jti": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","iss": "https://sso.example.com/realms/example-realm","aud": "client-app","sub": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","typ": "ID","azp": "client-app","nonce": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","sid": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","at_hash": "xxxxxxxxxxxxxxxxxxx","acr": "1","email_verified": false,"organization": {"XYZ科技有限公司": {"id": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}},"preferred_username": "user001","email": "user@example.com"
}
- 头部信息:
{"alg": "RS256","typ": "JWT","kid": "NGSiI_xOS-bWMHGgLp0aKgSdfC28LkbYjWwKUv5lXh8"
}
载荷 payload 详解
下面我将逐项解释每个参数的作用和应用场景:
JWT标准字段
-
exp (Expiration Time): 1755849788
- 令牌过期时间(
Unix
时间戳) - 用于确保令牌不会永久有效,增强安全性
- 令牌过期时间(
-
iat (Issued At): 1755849788
- 令牌签发时间
- 用于跟踪令牌的生命周期和审计
-
auth_time: 1755849788
- 用户实际认证时间
- 用于判断用户认证的新鲜度,防止使用很久之前的认证
-
jti (JWT ID): “xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”
JWT
唯一标识符- 用于防止令牌重放攻击
-
iss (Issuer): “https://sso.example.com/realms/example-realm”
- 令牌签发者(
Keycloak
域地址) - 用于验证令牌来源的合法性
- 令牌签发者(
-
aud (Audience): “client-app”
- 令牌目标受众(客户端应用)
- 确保令牌只能被指定的应用使用
-
sub (Subject): “xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”
- 令牌主体(用户唯一标识)
- 标识令牌是为哪个用户签发的
-
typ (Type): “ID”
- 令牌类型(
ID Token
,身份令牌) - 区分是
ID Token
还是Access Token
- 令牌类型(
-
azp (Authorized Party): “client-app”
- 实际请求方(客户端
ID
) - 在
OAuth2
授权流程中标识哪个客户端请求了此令牌
- 实际请求方(客户端
Keycloak 特有字段
-
nonce: “xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”
- 随机数
- 用于防止重放攻击,确保请求的唯一性
-
sid: “xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”
- 会话
ID
- 用于跟踪用户的会话状态
- 会话
-
at_hash: “xxxxxxxxxxxxxxxxxxx”
Access Token
哈希值- 用于验证
ID Token
和Access Token
的关联性
-
acr (Authentication Context Class Reference): “1”
- 认证上下文引用
- 表示认证强度级别
用户信息字段
-
email_verified: false
- 邮箱是否已验证
- 用于判断用户邮箱的有效性
-
organization:
- 用户所属组织信息
- 用于多租户或组织架构管理场景
-
preferred_username: “user001”
- 用户首选用户名
- 用于显示用户友好名称
-
email: “user@example.com”
- 用户邮箱地址
- 用于用户联系和识别
这些信息主要用于单点登录(SSO
)、用户身份验证、权限控制和审计跟踪等场景。
请求头解析 token
完整的 JWT
包含 头部(Header), 载荷(Payload),签名(Signature) 三部分组成:
- 数据格式如下:
# 每一部分使用符号 “.” 连接
头部.载荷.签名
# 示例数据
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
如何使用 .net 解析完整的 jwt
- 构建
jwt
对应的数据结构
namespace Data.Models;/// <summary>
/// keycloak 签发的 jwt 信息
/// </summary>
public sealed class JwtTokenInfo
{public JwtHeaderInfo Header { get; set; } = new();public JwtPayloadInfo Payload { get; set; } = new();public static JwtTokenInfo Empty() => new();
}// 头部信息
public sealed class JwtHeaderInfo
{// 算法 (alg)public string Algorithm { get; set; } = string.Empty;// 类型 (typ)public string Type { get; set; } = string.Empty;// 密钥ID (kid)public string KeyId { get; set; } = string.Empty;// 所有头部信息public Dictionary<string, object> JwtHeaders { get; set; } = [];
}// 载荷(负载)信息
public sealed class JwtPayloadInfo
{// 标准声明public DateTime? Expiration { get; set; } // exppublic DateTime? IssuedAt { get; set; } // iatpublic DateTime? AuthTime { get; set; } // auth_timepublic string JwtId { get; set; } = string.Empty; // jtipublic string Issuer { get; set; } = string.Empty; // isspublic string Audience { get; set; } = string.Empty; // audpublic string Subject { get; set; } = string.Empty; // sub// OpenID Connect声明public string Type { get; set; } = string.Empty; // typpublic string AuthorizedParty { get; set; } = string.Empty; // azppublic string Nonce { get; set; } = string.Empty; // noncepublic string SessionId { get; set; } = string.Empty; // sidpublic string AccessTokenHash { get; set; } = string.Empty; // at_hashpublic string AuthenticationContextClass { get; set; } = string.Empty; // acr// 用户相关信息public bool? EmailVerified { get; set; } // email_verifiedpublic OrganizationInfo Organization { get; set; } = new(); // organizationpublic string PreferredUsername { get; set; } = string.Empty; // preferred_usernamepublic string Email { get; set; } = string.Empty; // email// 所有声明public Dictionary<string, string> JwtClaims { get; set; } = [];
}// 组织or租户信息
public sealed class OrganizationInfo
{public string Name { get; set; } = string.Empty;public string Id { get; set; } = string.Empty;public static OrganizationInfo Empty() => new();
}
- 构建
jwt
解析服务IJwtParserService
/// <summary>
/// JWT解析服务
/// </summary>
public interface IJwtParserService
{/// <summary>/// 获取 Bearer Token/// </summary>/// <returns></returns>string GetBearerToken();/// <summary>/// 解析 Token/// </summary>/// <param name="token"></param>/// <returns></returns>JwtTokenInfo ParseToken(string token);
}
- 实现
jwt
解析服务
说明:此处需要安装 nuget
包
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package System.Text.Json
服务实现如下:
using Data.Models;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;namespace Services;public class JwtParserService(IHttpContextAccessor httpContextAccessor) : IJwtParserService
{private readonly string _authorization = "Authorization";private readonly string _jwtBearer = "Bearer ";public string GetBearerToken(){var httpContext = httpContextAccessor.HttpContext;if (httpContext == null)return string.Empty;// 从Authorization头获取Bearer令牌var authorizationHeader = httpContext.Request.Headers[_authorization].FirstOrDefault();if (string.IsNullOrWhiteSpace(authorizationHeader) || !authorizationHeader.StartsWith(_jwtBearer))return string.Empty;return authorizationHeader.Substring(_jwtBearer.Length).Trim();}public JwtTokenInfo ParseToken(string token){if (string.IsNullOrWhiteSpace(token)){return JwtTokenInfo.Empty();}try{var handler = new JwtSecurityTokenHandler();var jwtToken = handler.ReadJwtToken(token);return new JwtTokenInfo{Header = new JwtHeaderInfo{Algorithm = jwtToken.Header.Alg,Type = jwtToken.Header.Typ,KeyId = jwtToken.Header.Kid, // 获取Key IDJwtHeaders = jwtToken.Header.Where(h => h.Key != null).ToDictionary(h => h.Key, h => h.Value) // 包含所有头部信息},Payload = new JwtPayloadInfo{Expiration = jwtToken.Payload.Expiration.HasValue ? DateTimeOffset.FromUnixTimeSeconds(jwtToken.Payload.Expiration.Value).DateTime : null,IssuedAt = jwtToken.Payload.IssuedAt,AuthTime = GetClaimAsDateTime(jwtToken, "auth_time"),JwtId = jwtToken.Payload.Jti,Issuer = jwtToken.Payload.Iss,Audience = jwtToken.Payload.Aud.FirstOrDefault() ?? string.Empty,Subject = jwtToken.Payload.Sub,Type = GetClaimValue(jwtToken, "typ"),AuthorizedParty = GetClaimValue(jwtToken, "azp"),Nonce = GetClaimValue(jwtToken, "nonce"),SessionId = GetClaimValue(jwtToken, "sid"),AccessTokenHash = GetClaimValue(jwtToken, "at_hash"),AuthenticationContextClass = GetClaimValue(jwtToken, "acr"),EmailVerified = GetClaimAsBool(jwtToken, "email_verified"),Organization = GetOrganizationInfo(jwtToken),PreferredUsername = GetClaimValue(jwtToken, "preferred_username"),Email = GetClaimValue(jwtToken, "email"),JwtClaims = jwtToken.Payload.Claims.ToDictionary(c => c.Type, c => c.Value)}};}catch (Exception ex){throw new InvalidOperationException("Failed to parse JWT token", ex);}}private string GetClaimValue(JwtSecurityToken token, string claimType){return token.Payload.Claims.FirstOrDefault(c => c.Type == claimType)?.Value ?? string.Empty;}private DateTime? GetClaimAsDateTime(JwtSecurityToken token, string claimType){var claimValue = GetClaimValue(token, claimType);if (long.TryParse(claimValue, out long unixTime)){return DateTimeOffset.FromUnixTimeSeconds(unixTime).DateTime;}return null;}private bool? GetClaimAsBool(JwtSecurityToken token, string claimType){var claimValue = GetClaimValue(token, claimType);if (bool.TryParse(claimValue, out bool result)){return result;}return null;}private OrganizationInfo GetOrganizationInfo(JwtSecurityToken token){var orgClaim = token.Payload.Claims.FirstOrDefault(c => c.Type == "organization");if (orgClaim != null){try{// 解析组织信息(JSON格式)using var doc = JsonDocument.Parse(orgClaim.Value);var root = doc.RootElement.EnumerateObject().FirstOrDefault();return new OrganizationInfo{Name = root.Name,Id = root.Value.GetProperty("id").GetString() ?? string.Empty};}catch{// 如果解析失败,返回空实体return OrganizationInfo.Empty();}}return OrganizationInfo.Empty();}
}
解析 jwt 验证
Program.cs
注入解析服务
// 添加 HttpContextAccessor
builder.Services.AddHttpContextAccessor();// 注册JWT解析服务
builder.Services.AddScoped<IJwtParserService, JwtParserService>();
- 请求头注入
Authorization
# 数据格式
Authorization:Bearer token
请求头携带 jwt
数据:
- 使用
Minimal API
验证JWT
令牌
// 登录端点 - 从 Authorization 头获取并解析 JWT
app.MapGet("/auth/login", (IJwtParserService jwtParserService, ILogger<Program> logger) =>
{string token = jwtParserService.GetBearerToken();var tokenInfo = jwtParserService.ParseToken(token);string orgId = tokenInfo.Payload.Organization.Id;// 这里监控 tokenInfo
});
总结
本文详细绍了如何在 .NET9
环境中解析 Keycloak 26.2
签发的 JWT
令牌。通过 System.IdentityModel.Tokens.Jwt
库,我们可以轻松提取 JWT
的 头部、载荷和签名 信息。
重点解析了 JWT
中的 标准字段(exp、iat、iss等) 和 Keycloak 特有字段(organization、preferred_username等) 的含义及应用场景。实现了一个完整的 JwtParserService
服务,能够从 HTTP
请求头中提取 Bearer令牌
并解析出完整的用户信息,包括组织架构等扩展数据。
该方案支持 Minimal API
和传统控制器模式,为 ASP.NET Core
应用集成 Keycloak
单点登录提供了实用的解决方案,可广泛应用于企业级身份认证和权限管理系统中。