切换深色模式
第 2 章:JWT 签名正确就可信吗?从身份对象追到信任根
本章主线:从“验签成功即可相信”这个常见误区,逐层追踪主体、账号、凭证、证书、issuer、audience 与密钥生命周期。
先做一道判断题
小安收到一个签名完全正确、尚未过期的 JWT。它由公司某个测试环境 IdP 签发,aud 是另一个 API。当前生产 API 能接受吗?
不能。签名只证明内容由对应私钥产生且未被修改;是否可信还取决于信任的 issuer、预期 audience、用途、时间与业务上下文。下面先建立对象模型,再拆每一种密码学能力。
学习目标
能够选择正确密码学原语;实现严格 token 验证;解释 PKI 信任链;设计密钥轮换;在泄漏时判断影响面。本文只讲工程使用,不尝试自创密码算法。
2.1 五个容易混淆的对象
图 2-1:2.1 五个容易混淆的对象 · 可编辑 Mermaid 源码
- 主体:承担权利与责任的人或工作负载。
- 身份:与主体有关的一组稳定标识和属性。
- 账号:某系统内的操作载体,可禁用/删除。
- 凭证/认证器:证明控制权的机制,如私钥、密码、Passkey。
- 会话:一次认证后在有限时间内延续的上下文。
删除账号不一定意味着删除所有个人信息;丢失认证器也不等于现实主体消失。
2.2 信任从哪里开始
每条信任链都有根:政府证件、企业 HR、设备 TPM、云平台实例证明、CA 根密钥、人工审批。问“为什么相信它”并持续追到组织愿意承担的根。
信任根必须具备:受控创建、最小访问、备份/恢复、轮换、吊销、审计和职责分离。越靠近根,变更越谨慎。
2.3 哈希、MAC、加密、签名
| 原语 | 使用密钥 | 提供什么 | 身份常见用途 |
|---|---|---|---|
| 哈希 | 无 | 完整性摘要,不证明作者 | 指纹、内容寻址 |
| KDF | 密码+盐 | 抵抗离线猜测 | 密码存储 |
| MAC | 共享密钥 | 完整性+共享方认证 | Cookie/内部消息签名 |
| 对称加密 | 共享密钥 | 机密性 | 数据/备份加密 |
| 非对称加密 | 公私钥 | 机密性 | 密钥封装等 |
| 数字签名 | 私钥签、公钥验 | 完整性+持钥者认证 | JWT、证书、Passkey |
签名不提供机密性;加密不自动提供完整性;哈希不等于匿名化。
2.3.1 选择题式推导
- 要检测文件是否变化,但不关心作者:哈希。
- 两个受控服务共享秘密并验证消息来源:HMAC;规模扩大时注意共享密钥分发。
- 数据库字段落盘需要保密:经认证加密(如 AES-GCM),nonce 不重复。
- 一个 IdP 给许多 API 签 token,API 不应持有签名能力:非对称签名。
- 保存用户密码:密码 KDF,不是普通哈希也不是加密。
“算法安全”不代表系统安全。nonce 重用、错误 key scope、弱随机数、明文日志和不验证错误都会破坏性质。
2.4 密码存储
正确模型:服务保存 KDF(password, unique_salt, cost),可选 pepper 放在独立 KMS/HSM。优先 Argon2id 等内存困难算法,参数通过性能测试设置并版本化。
登录时需要统一错误信息、账号/来源限速、撞库检测、泄漏密码检查和安全重置。不要对密码做可逆加密,也不要用快速 SHA-256 直接存储。
2.4.1 参数升级
哈希记录应包含算法和参数版本。用户成功登录时,如果旧参数低于当前标准,在验证后立即用新参数重算;不需要知道明文之外的其他迁移手段。
text
$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>参数通过目标硬件压测选择:合法登录延迟可接受,同时让离线批量猜测昂贵。不同系统不要机械复制固定数字。
2.5 公钥基础设施 PKI
图 2-2:2.5 公钥基础设施 PKI · 可编辑 Mermaid 源码
验证证书不能只看“能解码”:检查链、用途、名称/SAN、有效期、算法、信任锚和撤销策略。短期证书可降低撤销依赖,但必须保证自动续期与时钟可靠。
2.5.1 证书验证逐步检查
- 构建到配置中信任锚的链,而非接受对端自带任意根;
- 验证每级签名和有效期;
- 检查 Basic Constraints、Key Usage、Extended Key Usage;
- 服务端证书匹配目标 DNS SAN;
- 按场景处理 name constraints、policy 和撤销;
- 将证书身份映射到稳定业务主体,再做授权。
常见事故是“证书合法但身份不对”:例如任何公司内部证书都能访问管理 API,因为服务只验证 CA,没有限制 SPIFFE ID 或 SAN。
2.6 TLS 与 mTLS
TLS 提供传输机密性、完整性和服务端认证。mTLS 额外认证客户端,适合服务间连接,但证书身份仍需映射到业务权限。
常见错误:关闭 hostname verification;信任任意自签名证书;客户端证书共享;私钥落盘权限过宽;网关终止 TLS 后盲信可伪造身份头。
2.7 JWT/JWS/JWE
JWT 是声明容器;JWS 是签名;JWE 是加密。收到 JWS token 的验证顺序:
- 由配置决定允许算法,不盲信 header;
- 按可信 issuer 选择密钥;
- 验签;
- 验
iss、aud、exp、nbf; - 按需验证
nonce、azp、jti、授权上下文; - 再使用业务 claims。
JWT 的缺点是签发后难实时撤销、内容易被误认为加密、claim 容易膨胀。高风险系统可用短有效期、token introspection、会话版本或连续信号补足。
2.7.1 验证伪代码
text
function verifyAccessToken(raw, expectedIssuer, expectedAudience):
header = parseProtectedHeader(raw)
require header.alg in configuredAlgorithms
key = trustedKeyStore(expectedIssuer).select(header.kid)
claims = verifySignature(raw, key)
require constantTimeEqual(claims.iss, expectedIssuer)
require expectedAudience in claims.aud
require now within [claims.nbf - skew, claims.exp + skew]
require claims.token_use == "access"
return claims解析未验签 claims 只能用于选择候选配置,不能据此授权。若支持多个 issuer,先按受控租户配置定位,不从任意 URL 动态下载 JWKS。
2.7.2 负向测试矩阵
错误算法、未知 kid、伪造 issuer、错 audience、过期、未来 nbf、重复 jti、签名截断、claims 类型错误、JWKS 轮换和网络失败都应有确定拒绝结果。
2.7.3 验证失败日志示例
json
{
"event": "access_token_rejected",
"reason_code": "TOKEN_AUDIENCE_MISMATCH",
"issuer": "https://id.example.com",
"expected_audience": "orders-api",
"presented_audience_hash": "sha256:...",
"key_id": "k2",
"request_id": "req-91a7"
}不记录完整 token 或全部 claims。监控未知 kid、验签失败、issuer/audience 错误、过期比例和 JWKS 刷新失败;配置发布后突然增长通常说明轮换或环境配置问题,来源分散增长也可能是攻击探测。
2.8 密钥管理生命周期
text
生成 → 注册/分发 → 使用 → 轮换 → 吊销 → 销毁 → 证据保留密钥应有 owner、用途、算法、创建/过期时间、存储位置、调用权限和轮换手册。应用通过 KMS/HSM 调用而不是导出主密钥。轮换需支持新旧 key overlap,先发布验证钥再用新签名钥。
2.8.1 零停机轮换时间线
text
T0 生成 K2,仍用 K1 签名
T1 JWKS 发布 K1+K2,等待缓存上限
T2 改用 K2 签名,验证端仍接受 K1+K2
T3 等待所有 K1 token 过期并留安全余量
T4 从 JWKS 移除 K1,销毁/归档旧私钥紧急泄漏不能等待正常时间线:停止 K1、撤销相关会话、缩短/拒绝旧 token、排查签名伪造、通知依赖方并保留证据。
2.9 身份保证等级
NIST 将身份保证拆为 IAL(身份核验)、AAL(认证)和 FAL(联邦)。业务按潜在损害选择强度,不应把所有用户都强制到最高等级,也不能用强 MFA 掩盖薄弱身份核验。
2.10 本章实验与验收
实验:生成 CA/服务证书并搭建 mTLS;签发 JWT 后逐一修改 header、payload、audience、时间和 key;观察正确验证器如何失败。
验收:解释签名为何不隐藏 JWT;为何 TLS 不能证明最终用户身份;如何零停机轮换签名密钥;密码盐为何可公开而 pepper 不应与数据库同存。
2.10.1 实验记录要求
保存证书链、验证命令输出、每个失败 token 的 reason code、轮换前后成功率和时序图。不要提交私钥;实验仓库只放生成脚本和无敏感公钥材料。
工程复盘:验签成功的测试 Token 为什么进了生产
小安第一次写 JWT 中间件时,只根据 kid 从共享 JWKS 中找公钥。单元测试全部通过,但负向测试把测试环境签发、面向另一个 API 的 token 发给生产服务时,中间件仍然接受了它。
他把拒绝过程拆成可观察的检查链,而不是只返回一个笼统的 401:
text
signature=valid
issuer=rejected(test-idp)
audience=rejected(order-api expected, profile-api actual)
token_use=rejected(id_token)
decision=deny修复不只是多写三个 if。团队为允许的 issuer 建立显式配置;按 issuer 隔离 JWKS 缓存;限制算法;校验 aud/azp/exp/nbf;为时钟偏差设定有界容忍;把每个拒绝理由做成不泄露敏感信息的内部 reason code。密钥轮换也加入“双 key 重叠—停止旧 key 签发—等待旧 token 过期—移除旧公钥”的演练。
这次实验让小安记住:密码学原语回答“有没有被篡改”,信任配置才回答“当前系统是否应该接受”。
面试与设计评审
- “JWT 与 Session 谁更安全?”不要二选一;比较撤销、状态、一致性、泄漏后果和架构需求。
- “签名和加密有什么区别?”先给完整性/来源与机密性的区别,再说明 AEAD/JWS/JWE。
- “怎么轮换密钥不停机?”说明先发布验证钥、新旧 overlap、切换签发、观察、退役与应急回滚。
本章小结
密码学原语提供有限性质,不替你做信任决策。每次看到 token、证书或哈希,都问它证明什么、不证明什么、密钥由谁控制、何时轮换和怎样撤销。