JWT 解析
这是一个 Keycloak 26.2
签发的 JWT (JSON Web Token)
载荷部分(payload
)的内容:
json
{
"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"
}
- 头部信息:
bash
{
"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) 三部分组成:

- 数据格式如下:
bash
# 每一部分使用符号 "." 连接
头部.载荷.签名
# 示例数据
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
如何使用 .net 解析完整的 jwt
- 构建
jwt
对应的数据结构
csharp
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; } // exp
public DateTime? IssuedAt { get; set; } // iat
public DateTime? AuthTime { get; set; } // auth_time
public string JwtId { get; set; } = string.Empty; // jti
public string Issuer { get; set; } = string.Empty; // iss
public string Audience { get; set; } = string.Empty; // aud
public string Subject { get; set; } = string.Empty; // sub
// OpenID Connect声明
public string Type { get; set; } = string.Empty; // typ
public string AuthorizedParty { get; set; } = string.Empty; // azp
public string Nonce { get; set; } = string.Empty; // nonce
public string SessionId { get; set; } = string.Empty; // sid
public string AccessTokenHash { get; set; } = string.Empty; // at_hash
public string AuthenticationContextClass { get; set; } = string.Empty; // acr
// 用户相关信息
public bool? EmailVerified { get; set; } // email_verified
public OrganizationInfo Organization { get; set; } = new(); // organization
public string PreferredUsername { get; set; } = string.Empty; // preferred_username
public 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
csharp
/// <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
包
csharp
dotnet add package System.IdentityModel.Tokens.Jwt
dotnet add package System.Text.Json
服务实现如下:
csharp
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 ID
JwtHeaders = 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
注入解析服务
csharp
// 添加 HttpContextAccessor
builder.Services.AddHttpContextAccessor();
// 注册JWT解析服务
builder.Services.AddScoped<IJwtParserService, JwtParserService>();
- 请求头注入
Authorization
bash
# 数据格式
Authorization:Bearer token
请求头携带 jwt
数据:

- 使用
Minimal API
验证JWT
令牌
csharp
// 登录端点 - 从 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
单点登录提供了实用的解决方案,可广泛应用于企业级身份认证和权限管理系统中。