第 5 / 21 篇

第4章 JWT 验证是一组约束,不是一次解码

解出 JSON 并没有认证任何人

JWT 的头和载荷常用 Base64URL 编码。任何人都能读,也能重新编码一份;安全性来自签名验证和完整声明约束。日志里打印出的 sub=alice 只有在签名、签发方、受众、时间和令牌类型均通过时,才可以成为身份事实。把“成功解码”命名成 authenticatedUser 会在代码层面制造误导。

本书使用成熟 JWT 库完成签名与解析,应用层显式限定算法。教学路径只允许 EdDSA,Keycloak 路径只允许 RS256。验证器不根据令牌头部的 alg 自行开放其他算法,也不接受 none。算法白名单属于接收方配置,而不是发送者可以协商的能力。RFC 8725 还强调针对不同 JWT 使用场景采用互斥的验证规则,以减少令牌替换和跨上下文混淆。

issuer、audience 与时间分别证明什么

issuer 标识受信签发方。它必须与配置精确匹配,不能因为域名相似、路径接近或 TLS 成功就自动信任。audience 标识令牌预期接收者;面向模型网关的令牌不应被订单服务接受。实验进一步要求教学令牌只有一个 audience,避免把多受众令牌当成默认便利工具。真实身份平台可能使用多个 audience,此时需要针对场景明确验证。

expiry 限制被盗凭据的最长暴露期,但只有接收方强制要求并验证它时才有效。not-before 限制提前使用,issued-at 帮助识别异常签发时间与生命周期。实验教学凭据最长两分钟,代理令牌九十秒,业务令牌三十秒,且委托后的令牌不会晚于父凭据到期。为了让测试确定,教学路径不设置时钟宽容;生产系统应基于可信时间同步设置有限容差,不能用几分钟容差掩盖时钟持续漂移。

jti 在本书用于保证凭据具有唯一标识,但它本身不阻止重放。只要服务没有维护相应状态,被盗 bearer 令牌在有效期内仍可再次使用。审批的一次性消费是另一个存储与事务问题,不能因为令牌里有一个随机 ID 就宣称解决。

密钥选择不能成为网络请求入口

教学 JWT 带 kid,验证器从固定身份服务的 /jwks 获取公钥。它忽略 token 提供的 jku、x5u 等可能指向 URL 的头字段。若验证器允许令牌自己指定 JWKS 地址,攻击者可以让它下载攻击者公钥,或探测内部网络。固定信任配置是验证前提,不应被待验证数据反过来改变。

公钥结构也需要限制。Ed25519 公钥长度必须正确;RSA 模数与指数需要基本结构检查。kid 只能在已信任密钥集合中选择,未知 kid 应在受控刷新后拒绝,不能回退到“取第一把钥匙试试”。生产缓存应限制刷新频率,防止攻击者用随机 kid 把身份平台打成每请求一次的放大器。

失败实验与实现边界

运行 go test -race -run TestSignedHTTPBoundaries -v ./lab。测试使用真实私钥构造“签名正确但声明错误”的令牌,这比只测试损坏签名更接近集成事故。缺少 expiry、未来 not-before、错误 issuer 和错误 audience 必须各自失败。对有效令牌破坏签名也必须失败,证明代码确实走过验签过程。

本书没有实现 RFC 9068 完整的 OAuth JWT Access Token Profile,也没有把自定义 actor 字段等同于 RFC 8693 的 act 对象。教学 JWT 的格式是本项目契约;标准解释用于帮助读者定位与成熟产品的差异。生产迁移应根据实际身份平台令牌类型和官方库配置验证器,不要仅复制字段名。

练习:删除验证器的 audience 限制,运行测试观察哪一项失败,再还原代码。接着写下你的业务 API 是否区分 Access Token、ID Token、刷新令牌和内部委托凭据。如果只用一个通用 parseJWT 接受它们,应设计独立入口和验证配置。

从失败原因定位集成问题

遇到验签失败时,不要先把所有约束关闭。可以先确认令牌来自哪个环境,再检查本地配置的 issuer 是否精确一致,随后核对 audience、算法、kid 与时间。开发环境与生产环境常有相似域名和不同路径,字符串上的细微差异正是信任边界的一部分,不应通过模糊匹配“修复”。

调试工具应输出安全的错误类别和请求引用,避免输出完整令牌。若需要查看声明,可以在受控本地环境解码脱敏样本,但仍要记住解码结果没有认证意义。生产告警中可以记录未知 kid 的计数与哈希,不应把攻击者可控的超长 kid 原样塞入结构化索引,造成日志膨胀或控制字符污染。

还应测试令牌输入长度和请求头大小。密码学库正确,不代表应用可以无限接收大令牌。实验先限制令牌长度,再进入解析;HTTP 服务也限制头部。生产反向代理、服务框架与业务验证器的限制应一致,否则上游接受而下游拒绝会形成难以诊断的失败,或被用来消耗不同层资源。