JWT 验证:为身份验证和授权验证令牌
关于 JWT 验证的实用指南 —— 该过程检查 JSON Web Token 的签名、声明和结构,以确认请求已通过真正的身份验证和授权 —— 涵盖签名验证、标准声明检查、密钥轮换、在 ASP.NET Core 中的验证,以及最常导致验证失败或被绕过的错误。
目录
- 简介
- JWT 的结构
- 签名算法
- “验证”实际检查的内容
- 签名验证与密钥轮换
- 标准声明验证
- 在 ASP.NET Core 中验证 JWT
- 自定义验证逻辑
- 令牌吊销:JWT 的根本局限
- 跨服务验证 JWT
- 常见漏洞
- 调试验证失败
- 快速参考表
- 结语
简介
通过 Authorization: Bearer <token> 头传递的 JWT 在被真正验证之前仅是一个字符串 —— 且验证所做的工作远超表面所见。它并非仅仅检查“这是否像 JWT”,甚至不只是“签名是否有效”—— 正确的验证会确认令牌由受信任方签发、专为该特定 API 发放、仍在有效期内,且内容未被篡改。若任何一项检查出错或被跳过,都可能导致 API 接受本不该接受的令牌。
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = "api://my-api";
});
Enter fullscreen mode Exit fullscreen mode
这两行代码看似简单,却配置了一套真正完备的验证流水线 —— 本指南将详细说明该流水线实际执行的检查、每项检查的意义,以及当验证配置错误或因压力被绕过时最常出现的问题。
1. JWT 的结构
三部分,以点分隔
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImFwaTovL215LWFwaSIsImV4cCI6MTcyMTQwNDgwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└──────────────── header ────────────────┘└──────────────────────── payload ────────────────────────┘└──────── signature ────────┘
Enter fullscreen mode Exit fullscreen mode
头部
{ "alg": "RS256", "typ": "JWT", "kid": "abc123" }
Enter fullscreen mode Exit fullscreen mode
-
alg— 使用的签名算法(第 2 节)。 -
typ— 标识这是一个 JWT。 -
kid(密钥 ID)— 标识用于签名该令牌的具体密钥(可能有多个当前有效密钥),对密钥轮换至关重要(第 4 节)。
载荷(声明)
{
"iss": "https://authserver.com",
"sub": "1234567890",
"aud": "api://my-api",
"exp": 1721404800,
"iat": 1721401200,
"nbf": 1721401200,
"scp": "products.read products.write"
}
Enter fullscreen mode Exit fullscreen mode
关于令牌、其主体及其有效期的一组声明 —— 第 5 节将详细介绍。
签名
signature = Sign(base64url(header) + "." + base64url(payload), private_key)
Enter fullscreen mode Exit fullscreen mode
使用签发者的私钥对头部和载荷共同计算得出的加密签名 —— 验证方只需使用签发者对应的公钥即可确认令牌确实来自声称的签发者,且内容自签名后未被篡改(第 4 节)。
关键点:载荷未加密
echo "eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIn0" | base64 -d
# {"iss":"https://authserver.com"}
Enter fullscreen mode Exit fullscreen mode
JWT 的头部和载荷仅经过 base64url 编码,并未加密 —— 任何拦截令牌的人(或用户自己查看令牌)都能轻易解码并读取其中的所有声明。这是一个常见且严重的误解:JWT 提供完整性和真实性(可确认令牌未被篡改且确实来自签发者),但完全不提供机密性。切勿将敏感数据(密码、密钥或任何不应被令牌持有者或拦截者看到的其他信息)直接放入 JWT 的声明中。
2. 签名算法
非对称(OAuth2/OIDC 访问令牌和 ID 令牌的标准)
RS256 — RSA 签名搭配 SHA-256(最常见)
ES256 — ECDSA 签名搭配 SHA-256(签名更小,采用率上升)
Enter fullscreen mode Exit fullscreen mode
使用非对称算法时,授权服务器使用其私钥对令牌签名,而任何数量的资源服务器(API)均可使用对应的公钥验证签名 —— 该公钥由授权服务器通过 jwks_uri 发现端点公开(详见本系列的 OAuth2/OpenID Connect 指南)。正是这种非对称性使 JWT 适用于分布式系统:数十个独立的 API 均可验证令牌,而无需持有任何能签发新有效令牌的密钥。
对称(很少适用于 OAuth2/OIDC 场景)
HS256 — 使用 SHA-256 的 HMAC,签名和验证使用同一个共享密钥
Enter fullscreen mode Exit fullscreen mode
使用对称算法时,同一个密钥既用于签名也用于验证 —— 这意味着每个需要验证令牌的参与方都必须持有能创建有效令牌的密钥。对于单个、自包含的应用内部签发和验证自己的令牌,这没问题;但对于典型的 OAuth2/OIDC 场景(一个授权服务器、多个独立的资源服务器)而言,这是一个糟糕的选择,因为将签名密钥分发给每个需要验证令牌的 API,意味着它们也能伪造令牌。
alg: none 攻击 —— 以及为什么算法确认很重要
{ "alg": "none", "typ": "JWT" }
Enter fullscreen mode Exit fullscreen mode
JWT 规范在技术上允许 alg 值为 "none",即不带签名 —— 若验证器盲目信任令牌头部声称的算法(而非强制使用预期算法),则可能被诱骗接受完全未签名、可随意伪造的令牌。第 10 节将更详细地介绍此攻击及密切相关的算法替换攻击 —— 简而言之:验证器必须始终强制使用其预期的特定算法,绝不能仅信任令牌自报的 alg 头部。
3. “验证”实际检查的内容
正确实现的 JWT 验证过程会检查几个截然不同的项目,跳过其中任何一项都会破坏整个验证:
1. 结构有效性 —— 这是否为格式正确的 JWT(三个 base64url 段)?
2. 签名验证 —— 是否由我们信任的密钥签名,且内容自签名后未被篡改?
3. 签发者 (iss) —— 是否来自我们真正信任的授权服务器?
4. 受众 (aud) —— 该令牌是否专为本API 签发,而非其他 API?
5. 过期时间 (exp) —— 该令牌的有效期是否已过?
6. 生效时间 (nbf) —— 该令牌是否在生效时间之前被使用?
7. 算法确认 —— 签名算法是否为我们预期的算法,而非攻击者选择的算法?
Enter fullscreen mode Exit fullscreen mode
现代 JWT 库(第 6 节)在正确配置后会自动执行所有七项检查 —— 但单独理解每项检查对于正确配置库以及识别自定义或手写验证实现是否遗漏关键内容至关重要。
4. 签名验证与密钥轮换
获取公钥
GET https://authserver.com/.well-known/jwks.json
Enter fullscreen mode Exit fullscreen mode
{
"keys": [
{
"kid": "abc123",
"kty": "RSA",
"use": "sig",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt...",
"e": "AQAB"
}
]
}
Enter fullscreen mode Exit fullscreen mode
JWKS(JSON Web Key Set) 端点发布授权服务器当前用于签名的公钥 —— 验证器获取该文档(通常缓存一段合理时间,而非每次请求都获取),并使用与令牌 kid 头部匹配的密钥验证签名。
密钥轮换存在的原因,以及 kid 的重要性
旧密钥 (kid: "abc123"): 正在逐步淘汰,对轮换前签发的令牌仍有效,直至其自然过期
新密钥 (kid: "def456"): 正在主动签名新令牌
Enter fullscreen mode Exit fullscreen mode
作为安全最佳实践,授权服务器会定期轮换其签名密钥 —— 限制任何单个密钥的使用时长,并在密钥被泄露时提供干净的恢复路径。由于使用旧密钥签名的令牌在其自然过期前仍有效,JWKS 端点通常会在轮换窗口期内同时发布多个当前有效密钥,而每个令牌中的 kid 头部会告诉验证器应使用已发布密钥中的哪一个。
缓存密钥,但非永久
options.TokenValidationParameters.ConfigurationManager =
new ConfigurationManager<OpenIdConnectConfiguration>(metadataAddress, retriever)
{
AutomaticRefreshInterval = TimeSpan.FromHours(24),
};
Enter fullscreen mode Exit fullscreen mode
每次传入请求都获取 JWKS 文档既浪费资源又增加不必要的延迟 —— 验证器会将获取的密钥缓存一段合理时间,但需定期刷新(理想情况下,若令牌引用了未知的 kid,应能立即刷新),以免授权服务器端的合法密钥轮换导致 API 端出现大量验证失败。构建良好的 OIDC 客户端库(第 6 节)会自动处理此刷新逻辑 —— 这正是手写实现中容易出错的细节。
5. 标准声明验证
签发者 (iss)
options.TokenValidationParameters.ValidIssuer = "https://login.microsoftonline.com/{tenant-id}/v2.0";
Enter fullscreen mode Exit fullscreen mode
确认令牌确实由应用信任的授权服务器签发 —— 若缺少此检查,仅验证签名的验证器可能被诱骗接受来自不同、不受信任签发者的完全有效签名令牌(若该签发者的公钥可用于验证),这在多租户或配置错误场景中是真实风险(第 10 节将进一步介绍)。
受众 (aud)
options.TokenValidationParameters.ValidAudience = "api://my-api";
Enter fullscreen mode Exit fullscreen mode
确认令牌是专为本 API签发,而非其他恰好信任同一授权服务器的 API 或客户端应用。这是需要正确处理的最关键检查之一 —— 若缺少此检查,为其他内部 API 合法签发的令牌(但由同一受信任授权服务器签名)可能被重放至您的API,并通过签名/签发者验证,因为这两项检查都会真正成功;只有受众检查能捕获此特定情况。
过期时间 (exp) 与生效时间 (nbf)
exp: 1721404800 —— 此 Unix 时间戳后令牌无效
nbf: 1721401200 —— 此 Unix 时间戳前令牌无效(较少见,但用于为未来使用签发的令牌)
Enter fullscreen mode Exit fullscreen mode
直观的时间窗口检查 —— 但值得注意的是,大多数验证库会应用较小的时钟偏差容差(通常为几分钟),以适应签发服务器与验证服务器之间的微小时钟漂移,而非要求时钟精确同步到秒级。
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5); // .NET 默认值
Enter fullscreen mode Exit fullscreen mode
过于宽松的时钟偏差容差会显著延长已过期(或尚未生效)令牌可能仍被接受的实际窗口期 —— 对于真正高安全性的场景,值得有意调低该值,而非在未充分考虑的情况下保留过于宽松的默认值。
主体 (sub)
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
Enter fullscreen mode Exit fullscreen mode
通常不针对预期值进行“验证”(与签发者/受众不同),但这是应用内部应作为已认证用户的稳定、规范标识符使用的声明 —— 切勿使用 email 或 username 声明,因为这些在用户生命周期中可能发生变化,而 sub 通常不会。
6. 在 ASP.NET Core 中验证 JWT
JWT Bearer 身份验证处理程序
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = "api://my-api";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(2),
};
});
app.UseAuthentication();
app.UseAuthorization();
Enter fullscreen mode Exit fullscreen mode
设置 Authority 会触发 ASP.NET Core 自动获取 OIDC 发现文档和 JWKS 端点(第 4 节),并保持其刷新 —— 第 3 节中的所有七项检查随后会在每个传入请求上自动执行,无需直接手写任何签名验证或声明检查逻辑。
对端点要求身份验证
app.MapGet("/products", () => GetProducts()).RequireAuthorization();
Enter fullscreen mode Exit fullscreen mode
[Authorize]
public class ProductsController : ControllerBase { }
Enter fullscreen mode Exit fullscreen mode
RequireAuthorization()(最小 API)或 [Authorize](MVC 控制器)实际强制要求给定端点必须存在已通过验证的有效令牌 —— 仅配置 AddJwtBearer 处理程序本身不会拒绝未认证的请求;它需要与这些端点上的授权要求配对使用。
显式强制使用预期算法
options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };
Enter fullscreen mode Exit fullscreen mode
如第 2 节和第 10 节所述,显式限制可接受的签名算法(而非信任令牌头部声称的算法)是一个值得、低成本的加固步骤,尤其当验证代码路径可能跨多个身份提供程序或具有不同算法预期的配置共享时。
7. 自定义验证逻辑
在标准检查之外添加应用特定检查
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var tenantId = context.Principal?.FindFirstValue("tid");
if (tenantId != _expectedTenantId)
{
context.Fail("Token issued for an unexpected tenant.");
return;
}
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
if (await _userService.IsUserSuspendedAsync(userId!))
{
context.Fail("User account is suspended.");
}
}
};
Enter fullscreen mode Exit fullscreen mode
OnTokenValidated 事件在所有标准加密和声明检查成功后触发,为额外的、应用特定的验证提供钩子 —— 确认多租户令牌确实属于预期租户、根据您自己的数据库检查用户的当前账户状态(这是任何纯加密令牌验证都无法知道的信息),或从其他地方查找额外声明以丰富生成的 ClaimsPrincipal。
为什么这很重要:加密有效性不等于“此请求应被允许”
令牌可能通过第 3 节中的所有检查 —— 由受信任签发者真正签名、受众正确、未过期 —— 但仍可能代表不应被接受的请求,因为用户账户在令牌签发五分钟后被挂起,或因授权服务器不知道的其他业务规则。自定义验证逻辑正是填补这些空白之处,值得深思熟虑地考虑哪些应用特定检查应放在此处,而非假设“令牌已验证”就等同于“此请求应被允许”。
8. 令牌吊销:JWT 的根本局限
核心矛盾
JWT 是无状态验证的 —— 这是其全部吸引力所在,允许 API 在每次请求时无需返回授权服务器进行网络往返即可本地验证令牌的真实性(详见本系列的 OAuth2/OpenID Connect 指南)。但这一特性也意味着 JWT 一经签发,在自然过期前始终保持加密有效,即使授权服务器非常希望立即吊销它(用户已注销、管理员已禁用账户、令牌被检测为被盗)。
令牌已签发,exp: 1 小时后
5 分钟后:用户账户被挂起
……但令牌在剩余的 55 分钟内仍保持加密有效,除非采取额外措施
Enter fullscreen mode Exit fullscreen mode
缓解措施 1:保持访问令牌生命周期短
访问令牌生命周期:15 分钟(常见、合理的默认值)
Enter fullscreen mode Exit fullscreen mode
最常见且最简单的缓解措施是根本不让此窗口期过大 —— 短生命周期的访问令牌限制了“无法再信任但尚未正式过期”的令牌可用的时长,代价是更频繁的(对用户不可见,通过刷新令牌处理,详见 OAuth2/OpenID Connect 指南)令牌续期。
缓解措施 2:在自定义验证中检查吊销/拒绝列表
OnTokenValidated = async context =>
{
var jti = context.Principal?.FindFirstValue("jti"); // 唯一令牌标识符
if (await _revocationStore.IsRevokedAsync(jti!))
{
context.Fail("Token has been revoked.");
}
}
Enter fullscreen mode Exit fullscreen mode
对于即使是短过期窗口也无法接受的场景(例如在检测到账户泄露后立即吊销访问),维护显式吊销列表 —— 在自定义验证(第 7 节)期间通过令牌的唯一 jti(JWT ID)声明检查 —— 会重新引入 JWT 设计初衷所要避免的有状态、每次请求检查,但仅针对此特定、较窄的需求,而非令牌中的每个声明。
缓解措施 3:对真正需要即时吊销的场景使用不透明令牌
如本系列的 OAuth2/OpenID Connect 指南所述,不透明令牌(通过每次请求调用授权服务器的内省端点验证)完全绕过了此局限,因为授权服务器可以简单地立即停止识别被吊销的令牌 —— 直接代价是重新引入网络往返和对授权服务器可用性的依赖,而这正是 JWT 存在所要避免的。在 JWT 和不透明令牌之间做出选择,在很大程度上是对此权衡的选择:具有有界吊销延迟的本地、快速、无状态验证, versus 具有每次请求依赖的集中式、可即时吊销的验证。
9. 跨服务验证 JWT
gRPC/微服务场景
// 每个内部服务使用相同的共享 Authority 独立验证 JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://internal-idp.mycompany.com";
options.Audience = "api://internal-services";
});
Enter fullscreen mode Exit fullscreen mode
在微服务架构中(连接到本系列的 gRPC 和后台服务指南),每个单独的服务都可以使用同一共享授权服务器的公钥独立验证传入的 JWT —— 任何服务都不需要直接信任其他服务,或维护自己单独的凭证存储;它们都针对同一中央信任源进行验证,这正是 JWT 身份验证对大规模内部服务间通信具有吸引力的特性之一。
服务间令牌传播与重新签发
客户端 → API 网关(验证用户令牌)→ 服务 A(重新验证?还是信任网关?)
Enter fullscreen mode Exit fullscreen mode
值得深思熟虑的设计决策:内部服务是否重新验证已通过 API 网关或其他上游服务传递的令牌,还是信任上游验证已发生?在每个跃点重新验证更具防御性(防范被攻陷或配置错误的上游服务),但会增加延迟和复杂性;信任上游网关的验证更简单,但将信任(和风险)集中在该网关。许多组织使用服务网格(如本系列的 Kubernetes/Helm 指南中所述),在服务间使用双向 TLS,以便内部服务间调用具有自己的传输层信任,这在某种程度上独立于每个跃点是否单独重新验证原始用户的 JWT。
服务链的 On-Behalf-Of 流程
用户令牌 → 服务 A 将其交换为新令牌,代表用户调用服务 B
Enter fullscreen mode Exit fullscreen mode
对于中间服务需要作为原始用户(而非仅作为自身)调用下游服务的请求链,OAuth2 的On-Behalf-Of 流程允许服务将传入令牌交换为新令牌,作用域适当,适用于下一个跃点 —— 在链中保留原始用户身份,而非中间服务仅使用其自己的服务间凭证并丢失原始请求的对象是谁。
10. 常见漏洞
alg: none 和算法混淆攻击
// ❌ 易受攻击:盲目信任令牌自报的算法
var algorithm = tokenHeader["alg"];
// if algorithm == "none", skip signature check entirely — catastrophic
// ✅ 安全:验证器强制使用明确的预期算法,而非令牌声称的算法
options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };
Enter fullscreen mode Exit fullscreen mode
除了第 2 节提到的 alg: none 攻击外,相关的算法混淆攻击针对同时支持对称(HS256)和非对称(RS256)算法的验证器:知道 RS256 公钥的攻击者有时可以使用该公钥作为 HMAC 密钥,用 HS256 伪造令牌 —— 若验证器不严格强制使用其预期的特定算法,它可能会错误地使用其视为共享密钥的值验证伪造的 HS256 签名,而该“密钥”实际上是一个公开已知的值。现代、维护良好的 JWT 库默认会防范这两种攻击,但这正是手写 JWT 验证逻辑相对于使用成熟、活跃维护的库而言风险较高的微妙问题。
缺少受众验证
// ❌ 仅验证签名和签发者 —— 接受为完全不同的 API 签发的令牌
options.TokenValidationParameters.ValidateAudience = false;
Enter fullscreen mode Exit fullscreen mode
如第 5 节所述,跳过受众验证意味着为不同 API 合法签发的令牌(但由同一受信任授权服务器签发)可以被重放至您的 API —— 这是一个非常常见的错误配置,有时是在调试验证问题时引入的(“让我们禁用此检查以使其工作”),之后从未重新启用。
信任客户端提供的声明而未进行服务器端验证
// ❌ 切勿信任客户端可以提供未签名/未验证的角色/权限声明
var isAdmin = Request.Headers["X-User-Role"] == "admin";
// ✅ 仅信任来自加密验证令牌本身的声明
var isAdmin = User.HasClaim("role", "admin");
Enter fullscreen mode Exit fullscreen mode
任何授权决策都必须基于实际已验证、已签名的 JWT 中的声明 —— 而非客户端可以简单设置为任何值的单独、未签名的头部或参数。这在孤立情况下听起来显而易见,但在逐步演进的系统中却是一个非常常见的错误,在这些系统中,“临时”调试头部或客户端提供的参数悄然影响了真实的授权决策。
不验证令牌类型/用途
// 刷新令牌、ID 令牌和访问令牌都可以是形状相似的 JWT ——
// 没有什么能阻止客户端错误地(或恶意地)将错误的令牌发送到期望特定类型的端点
Enter fullscreen mode Exit fullscreen mode
如本系列的 OAuth2/OpenID Connect 指南中所述,ID 令牌和访问令牌服务于不同用途,通常具有不同的预期受众 —— 但若 API 不具体检查传入令牌的声明是否符合其自身用途的预期(正确的受众、存在的预期作用域),则存在接受实际上从未打算用于该用途的令牌的风险,即使该令牌在其他方面完全有效签名。
多租户场景中过于宽松的 ValidIssuers/ValidAudiences
// ❌ 接受来自该身份提供程序下任何租户的令牌,而非仅来自您的租户
options.TokenValidationParameters.ValidIssuer = null;
options.TokenValidationParameters.ValidateIssuer = false;
Enter fullscreen mode Exit fullscreen mode
在构建于共享身份提供程序(如 Microsoft Entra ID 的多租户应用注册)之上的多租户 SaaS 场景中,禁用或过度放宽签发者验证以“使多租户工作”可能会无意中允许来自任何租户用户的令牌对您的应用进行身份验证,而非仅允许已实际配置/同意的租户 —— 多租户验证需要有意识地、明确地允许有效租户签发者的白名单(或在自定义验证中进行等效的租户 ID 声明检查,第 7 节),而非简单地禁用检查。
11. 调试验证失败
读取实际失败原因
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
_logger.LogWarning(context.Exception, "JWT validation failed");
return Task.CompletedTask;
}
};
Enter fullscreen mode Exit fullscreen mode
默认情况下,ASP.NET Core 的 JWT Bearer 处理程序不会向客户端显示详细原因(良好的安全默认值 —— 通常不希望告诉攻击者其伪造令牌被拒绝的确切原因),但在服务器端记录实际异常对于在开发和生产事件响应期间诊断合法验证问题至关重要。
常见失败原因及其指示
| 错误 | 可能原因 |
|---|---|
IDX10223: Lifetime validation failed. The token is expired |
访问令牌自然过期 —— 客户端应已刷新它 |
IDX10214: Audience validation failed |
令牌是为与验证它的 API/客户端不同的对象签发的 |
IDX10205: Issuer validation failed |
令牌来自意外或不受信任的授权服务器 |
IDX10501: Signature validation failed. Unable to match key |
令牌中的 kid 与任何当前缓存的密钥不匹配 —— 可能是验证器尚未刷新的密钥轮换 |
IDX10223 相关的时钟相关错误 |
签发服务器与验证服务器之间的时钟偏差超过配置的容差 |
手动解码令牌以供检查(绝不用于生产验证)
# 拆分并 base64 解码载荷段,仅用于调试期间的人工检查
echo "<payload-segment>" | base64 -d | jq
Enter fullscreen mode Exit fullscreen mode
手动解码令牌的声明(通过 jwt.io 等工具,或快速 shell 单行命令)是在调试期间确认令牌实际包含哪些声明的有用、纯诊断步骤 —— 这绝不能替代应用代码中的实际加密验证,任何生产代码路径都不应将仅解码、未验证的令牌视为可信。
快速参考表
| 概念 | 用途 |
|---|---|
| 头部 / 载荷 / 签名 | JWT 的三个组成部分 |
kid |
标识用于签名的具体密钥,支持密钥轮换 |
RS256/ES256(非对称) |
OAuth2/OIDC 的标准 —— 一个签发者签名,许多验证者检查 |
HS256(对称) |
共享密钥签名,通常不适用于多方 OAuth2 场景 |
iss 验证 |
确认令牌来自受信任的授权服务器 |
aud 验证 |
确认令牌是专为本 API 签发的 |
exp/nbf 验证 |
确认令牌在其预期的有效期窗口内 |
| JWKS 端点 | 发布验证签名所需的公钥 |
OnTokenValidated |
用于在标准检查之外进行自定义、应用特定验证的钩子 |
吊销列表 / jti
|
为需要即时吊销的场景重新引入有状态检查 |
| 算法混淆攻击 | 利用不严格强制使用预期签名算法的验证器 |
结语
JWT 验证从外部看似乎非常简单 —— 检查签名,读取一些声明 —— 但正确实现的验证器正在悄无声息地执行七项不同的、各自重要的检查,其中几项(受众验证、算法强制、多租户场景中的签发者限制)正是那些在调试压力下意外被削弱或禁用且从未恢复的检查。实用指导与本系列其他与安全相关的指南一致:使用成熟、活跃维护的库(ASP.NET Core 的 AddJwtBearer,由 Microsoft.IdentityModel.Tokens 提供支持),而非手写签名验证或声明检查,让它通过 Authority 自动处理密钥轮换和发现,并使用其扩展点(OnTokenValidated)进行真正的应用特定检查 —— 如账户状态或租户范围 —— 这些是任何纯加密验证都无法自行知道的。
理解这些两三行配置之下实际发生的事情,是将“身份验证中间件抛出错误”从谜团转变为可解决问题,以及识别看似合理的验证配置何时悄无声息地禁用了本不应禁用的检查的关键。
觉得有用?欢迎为仓库加星、通过 issue 提交更正,或分享那个教你永远不要仅仅为了消除错误而禁用检查的受众验证错误。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.