.net9 解析 jwt 详解

JWT 解析

    • [载荷 payload 详解](#载荷 payload 详解)
    • [请求头解析 token](#请求头解析 token)
      • [如何使用 .net 解析完整的 jwt](#如何使用 .net 解析完整的 jwt)
      • [解析 jwt 验证](#解析 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 TokenAccess 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 单点登录提供了实用的解决方案,可广泛应用于企业级身份认证和权限管理系统中。