当前位置: 首页 > ops >正文

.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 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) 三部分组成:

在这里插入图片描述

  • 数据格式如下:
# 每一部分使用符号 “.” 连接
头部.载荷.签名
# 示例数据
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 单点登录提供了实用的解决方案,可广泛应用于企业级身份认证和权限管理系统中。

http://www.xdnf.cn/news/18607.html

相关文章:

  • Go语言 Hello World 实例
  • RabbitMQ--消费端异常处理与 Spring Retry
  • 2025最新ncm转MP3,网易云ncm转mp3格式,ncm转mp3工具!
  • ThinkPHP8学习篇(四):请求和响应
  • VSCode无权访问扩展市场
  • 【数据结构】-5- 顺序表 (下)
  • 【JavaEE】了解synchronized
  • Java 基础学习总结(211)—— Apache Commons ValidationUtils:让参数校验从 “体力活“ 变 “优雅事“
  • 电动车运行原理与最新人工智能驾驶技术在电动车上的应用展望:从基础动力系统到L5级完全自动驾驶的技术深度解析
  • 大语言模型的自动驾驶 LMDrive/DriveVLM-Dual
  • Kubernetes部署Prometheus+Grafana 监控系统NFS存储方案
  • Spark04-MLib library01-机器学习的介绍
  • Spring创建的方式
  • 在 Ubuntu 24.04 或 22.04 LTS 服务器上安装、配置和使用 Fail2ban
  • 【LLM】DeepSeek-V3.1-Think模型相关细节
  • Android - 用Scrcpy 将手机投屏到Windows电脑上
  • MySQL学习记录-基础知识及SQL语句
  • SSRF的学习笔记
  • React useState 全面深入解析
  • 6.2 el-menu
  • Axure RP 9的安装
  • 如何让FastAPI在百万级任务处理中依然游刃有余?
  • Postman参数类型、功能、用途及 后端接口接收详解【接口调试工具】
  • 【C++】函数返回方式详解:传值、传引用与传地址
  • Linux 824 shell:expect
  • 今日科技热点 | AI加速创新,5G与量子计算引领未来
  • PHP - 实例属性访问与静态方法调用的性能差异解析
  • B站视频字幕提取工具
  • mysql 5.7 查询运行时间较长的sql
  • 【计算机408数据结构】第三章:基本数据结构之栈