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

.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.dtcms.com/a/348528.html

相关文章:

  • Indy HTTP Server 使用 OpenSSL 3.0
  • 采摘机器人设计cad+三维图+设计说明书
  • 学习记录(二十一)-Overleaf中图片文字间隔太大怎么办
  • 【QT入门到晋级】进程间通信(IPC)-共享内存
  • Java数据结构——7.二叉树(总览)
  • 机器学习周报十
  • 从文本树到结构化路径:解析有限元项目架构的自动化之道
  • Rust Web开发指南 第二章(Axum 路由与参数处理)
  • gcc报错解决办法
  • Maxwell学习笔记
  • 如何让FastAPI在百万级任务处理中依然游刃有余?
  • Node【文件+模块化+对象】详讲:
  • OSG库子动态库和插件等文件介绍
  • k8s原理及操作
  • LLM 中评价指标与训练概要介绍
  • AI Prompt 的原理与实战
  • 【LeetCode】分享|如何科学的刷题?
  • 【深度学习】骨干网络(Backbone)
  • 毛选一卷解析
  • VAREdit:深度解读
  • k8s部署,pod管理,控制器,微服务,集群储存,集群网络及调度,集群认证
  • 在Excel和WPS表格中打印时加上行号和列标
  • rosdep无法获取noetic源?
  • 深入解析 std::enable_if:原理、用法与现代 C++ 实践
  • 维修工人Springboot社区家电服务小程序
  • [身份验证脚手架] 技术栈特定安装逻辑
  • 人形机器人——电子皮肤技术路线:光学式电子皮肤及MIT基于光导纤维的分布式触觉传感电子皮肤
  • Java 学习笔记(基础篇9)
  • 有哪些工具可以帮助监测和分析JVM的内存使用情况?
  • 前端漏洞(上)- Django debug page XSS漏洞(漏洞编号:CVE-2017-12794)