题干与适用场景
认证服务使用 RS256 签发访问令牌。令牌最长可被接受 15 分钟,允许 1 分钟时钟偏差。200 个后端服务从发行方的 OpenID Discovery 文档读取固定的 jwks_uri,把 JWKS 缓存 10 分钟并在本地验签。
请设计一套不会让合法请求集中出现 401 的签名密钥轮换方案,并覆盖以下情况:
- 正常轮换期间,新旧令牌同时到达;
- 缓存中找不到令牌头部的
kid; - 大量随机
kid试图触发 JWKS 刷新风暴; - JWKS 端点在轮换期间超时或返回错误;
- 当前私钥疑似泄露,需要紧急止损;
- 如何证明所有验证方已经接受新密钥,并安全退役旧密钥。
15 分钟、10 分钟、1 分钟和 200 个服务都是题目约束,不是通用推荐值。核心考察点是后端认证协议、缓存一致性、失败语义与安全运维,因此归为 backend。
面试官考察点
第一,候选人能否说出正确顺序:先发布新公钥,再让验证方看到它,然后才用新私钥签发;最后等待旧令牌过期,再移除旧公钥。 如果先切换签发器,尚未刷新 JWKS 的服务必然拒绝新令牌。
第二,是否理解 JWKS 是公钥集合。RFC 7517 定义 keys 数组,kid 只是从可信集合中选择密钥的标识。私钥不能发布到 JWKS;kid 也不是信任证明,必须同时绑定可信发行方、固定算法和签名校验。
第三,是否能平衡缓存与安全。每次请求都拉 JWKS 会压垮发行方,永久缓存则无法及时接受新密钥或停止信任旧密钥。未知 kid 可以触发一次受控刷新,但必须合并并发刷新、限速并短暂缓存确认不存在的标识。
第四,是否区分计划轮换与私钥泄露。计划轮换允许新旧公钥重叠以保护可用性;泄露时继续保留旧密钥会让攻击者伪造令牌。紧急流程需要显式缓存失效、撤销策略和更强的安全优先级。
第五,是否把验签扩展为完整验证。验证方应固定允许的算法,校验签名、iss、aud、exp 与 nbf,并拒绝来源不可信的 jku 或任意密钥地址,避免算法混淆与 SSRF。
回答前需要澄清的问题
- 令牌的最长接受时间是多少? 应使用所有可能被接受的旧令牌寿命,而不只看配置中的名义 TTL;还要包括时钟偏差。
- JWKS 的缓存上限是多少? 浏览器、CDN、代理和进程内缓存可能各有一层,轮换前要得到真实的最大陈旧时间。
- 能否主动刷新所有验证方? 若有配置推送、版本确认或金丝雀探测,就不必只靠等待缓存自然到期。
- 验证方在 JWKS 故障时如何处理? 需要明确已知密钥能否在有限时间内使用陈旧缓存,以及未知密钥是否严格拒绝。
- 是否要求立即撤销已签发令牌? 自包含 JWT 不能仅靠换密钥实现精确单令牌撤销,可能需要拒绝列表、令牌版本或内省。
- 谁控制发行方和验证方? 同一组织内可做刷新确认;第三方验证方通常只能依赖公开兼容窗口。
- 密钥放在哪里? 私钥应在 KMS、HSM 或受限密钥服务中生成和使用,发布面只包含公钥。
- 成功标准是什么? 计划轮换要满足合法新旧令牌持续通过;应急轮换则可能接受可控的重新登录,以尽快停止信任泄露密钥。
30 秒回答框架
“我把轮换拆成发布、预热、切签、重叠和退役五步。先生成带全新 kid 的密钥,把新公钥与旧公钥同时发布到 JWKS;等待最长 10 分钟缓存窗口,或让 200 个验证服务主动刷新并上报新 JWKS 版本。确认新公钥可用后,签发器才改用新私钥。
旧公钥继续保留到最后一个旧令牌签发后的 15 分钟寿命加 1 分钟时钟偏差结束。验证方按可信发行方与固定 RS256 选择 kid,校验签名、iss、aud、exp 和 nbf。未知 kid 只触发一次合并且限速的刷新,刷新后仍不存在就拒绝;JWKS 故障时,已知密钥可按明确的有限陈旧策略继续使用,未知密钥 fail closed。
若私钥泄露,我会停止旧密钥签发、发布并切换新密钥,同时广播缓存失效并撤销旧密钥签发的令牌;不能套用正常重叠窗口。最后用按 kid 的验签结果、缓存年龄、JWKS 拉取量和旧 kid 最后出现时间证明轮换完成。”
分步骤深入解答
第一步:固定信任根与验证不变量
验证服务从配置中的可信发行方读取 OpenID Discovery,再使用其中 HTTPS 的 jwks_uri。令牌头部的 kid 只用于查找这份可信 JWKS 中的候选公钥,不能让令牌通过 jku 指向任意地址,也不能把 kid 直接拼进文件、数据库或 URL 查询。
每次验证至少保持以下不变量:
- 只接受配置允许的
RS256,令牌不能自行协商算法; kid必须唯一匹配可信发行方 JWKS 中用途为签名的公钥;- 签名通过后继续校验预期
iss、当前 API 的aud、exp和nbf; - 任一验证失败都拒绝整个令牌;
- JWKS 只发布公钥材料,私钥始终留在受控签发边界内。
一个轮换中的最小公钥集合可以是:
{
"keys": [
{ "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "2026-07-a", "n": "...", "e": "AQAB" },
{ "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "2026-07-b", "n": "...", "e": "AQAB" }
]
}集合顺序不表示优先级;验证方按 kid 精确选择。新密钥必须使用新的、不复用的 kid,否则缓存无法区分相同标识下被替换的密钥材料。
第二步:按发布先于签发的顺序执行计划轮换
计划轮换可建模为以下状态:
GENERATED
-> PUBLISHED(old + new)
-> VERIFIER_READY
-> SIGNING_WITH_NEW
-> OLD_TOKEN_DRAINED
-> OLD_KEY_RETIRED具体流程如下:
- 在受控密钥系统中生成新密钥
2026-07-b,签发器尚不使用它; - 发布包含
2026-07-a与2026-07-b的 JWKS,并产生新的 ETag 或集合版本; - 等待 10 分钟最大缓存陈旧窗口,或主动刷新 200 个验证方并收集它们识别新
kid的确认; - 用金丝雀令牌验证各环境的签名、发行方、受众和时间声明;
- 只有新公钥已被验证方接受后,签发器才开始用
2026-07-b; - 记录最后一个旧密钥令牌的签发时间
Tlastold; - 至少到
Tlastold + 15 分钟 + 1 分钟后,且旧kid流量符合预期,再从 JWKS 移除旧公钥; - 继续监控异常旧
kid,最后禁用并销毁旧私钥材料。
“先发布、后签发”保护新令牌,“停止旧签发、等待旧令牌排空、再移除”保护旧令牌。缓存传播时间影响切签的最早时刻;旧令牌接受寿命与时钟偏差决定移除旧公钥的最早时刻。
第三步:设计未知 kid 的单次受控刷新
未知 kid 既可能是刚轮换的新密钥,也可能是攻击者构造的随机值。验证方不能立即拒绝所有未知值,也不能让每个未知值无限拉取 JWKS。可使用以下逻辑:
verify(token):
header = parse_bounded_header(token)
require header.alg == "RS256"
key = trusted_cache.find(header.kid)
if key is missing:
refresh trusted_issuer_jwks once through single-flight
key = trusted_cache.find(header.kid)
if key is missing:
short_negative_cache.add(header.kid)
reject "unknown kid"
verify signature and require iss, aud, exp, nbf同一发行方的并发 miss 应共享一个 single-flight 刷新。刷新受全局冷却时间和超时限制;在确认最新集合仍不包含该 kid 后,可对该值做短时负缓存,避免随机标识反复触发上游请求。负缓存不能长到妨碍真正轮换,且 kid 的长度和格式要先受限,防止高基数内存攻击。
刷新只能访问配置绑定的 jwks_uri,使用 TLS、合理的响应大小上限、连接与读取超时,并支持 ETag 条件请求。攻击者提供的 jku、x5u 或相似地址不得覆盖这个信任根。
第四步:明确 JWKS 故障时的可用性边界
正常请求应完全使用本地缓存,不把 JWKS 端点放入每次认证的同步路径。若刷新失败:
- 缓存中已有匹配
kid且密钥仍在事先定义的有限陈旧窗口内,可以继续验签; - 缓存中没有匹配
kid,必须拒绝,不能跳过签名或试遍不相关密钥; - 过了有限陈旧窗口仍无法刷新,应告警并按安全策略拒绝;
- 签发方若无法证明验证方已经拿到新公钥,就不能开始用新私钥签发。
有限的 stale-if-error 能吸收短时网络故障,但会延长已删除公钥的本地信任时间。这个窗口必须写进安全模型,并在密钥泄露时用显式失效机制覆盖。可用性不能依赖永久陈旧缓存。
第五步:为私钥泄露建立独立应急流程
泄露场景的目标是尽快停止接受攻击者可能伪造的令牌。处理顺序为:停止旧私钥签发,生成并发布新公钥,强制验证方刷新,切换新私钥,并让旧密钥立即进入撤销状态。此时不能为了无感体验继续正常重叠 16 分钟。
只从中心 JWKS 删除旧公钥仍不够,因为验证方可能保留 10 分钟缓存。需要控制面广播、缓存版本推送、服务重启或其他已演练的失效通道。若无法触达第三方验证方,只能依赖它们的缓存上限,并应在风险评估中明确暴露窗口。
换密钥也不会精确召回已经签发的自包含令牌。若业务要求立即撤销,可按旧 kid 或 iat 截止点维护临时拒绝策略,缩短访问令牌寿命,或对高风险接口使用内省/会话状态。应急处理可能迫使用户重新认证,这是安全优先下可接受的业务影响。
第六步:用版本、指标和审计证明完成
JWKS 响应应有可观察的集合版本或 ETag。验证服务上报当前版本、缓存年龄、刷新结果和识别到的 kid;签发服务记录当前签名 kid,但不记录令牌正文或私钥材料。
关键指标包括:按发行方、kid 和错误类型分组的验签结果,未知 kid 基数,JWKS 拉取次数、延迟与失败率,single-flight 合并数,缓存年龄,新 kid 金丝雀成功率,以及旧 kid 最后一次合法出现时间。若切签后新 kid 的 unknown-key 错误上升,应自动停止或回滚签发切换,而不是等待用户报错。
审计记录要能回答:谁在何时生成、发布、激活和退役了哪个密钥;当时 JWKS 版本是什么;哪些验证方确认就绪;旧密钥最后签发时间与退役依据是什么。密钥生命周期操作使用双人审批和最小权限更稳妥。
第七步:覆盖过渡态与攻击面的测试矩阵
测试不能只验证一枚静态有效令牌,至少应覆盖:
- 只发布旧公钥时,旧令牌通过、新令牌拒绝;
- 新旧公钥重叠时,两类令牌都通过,且重复验签不重复拉取 JWKS;
- 缓存仅有旧公钥时,新
kid触发一次刷新后通过; - 大量相同或随机未知
kid只产生受控数量的刷新,最终全部拒绝; - JWKS 返回超时、500、超大响应或非法 JSON 时,已知密钥按策略工作,未知密钥拒绝;
- 固定算法、错误
iss、错误aud、过期和尚未生效的令牌全部拒绝; - 令牌携带攻击者
jku时,验证方仍只访问配置的可信地址; - 旧公钥移除后,新进程拒绝旧令牌;仍有旧缓存的进程只在明示窗口内接受;
- 泄露演练中,失效信号能让所有受控验证方停止接受旧
kid; - 切签门禁在验证方未就绪时阻止新私钥启用。
验收要同时看授权结果和 JWKS 请求次数。令牌通过但每次都访问上游不是合格实现;请求量正常但合法新令牌被拒绝同样不合格。
高质量示范回答
“我先把 kid 定义为可信发行方 JWKS 内的选择器,不把它当信任来源。每个验证服务只接受配置允许的 RS256,从固定 Discovery 的 jwks_uri 获取公钥,按 kid 选钥后校验签名、iss、aud、exp 和 nbf。JWKS 只包含公钥,私钥留在 KMS 或 HSM 的签发边界。
计划轮换时,我先生成带新 kid 的密钥,把新旧公钥一起发布并更新 ETag。接着等待题目中的 10 分钟最大缓存窗口,或主动刷新 200 个验证服务并收集就绪状态,再用金丝雀令牌确认新密钥可验证。只有这一步通过,签发器才切到新私钥。
我会记录最后一枚旧令牌的签发时间。旧公钥至少保留到该时刻后的 15 分钟接受寿命加 1 分钟时钟偏差,并确认旧 kid 已排空后再移除。这样切换两边都有明确门禁:新公钥已传播才签新令牌,旧令牌失效后才撤旧公钥。
缓存 miss 时,同一发行方只允许一个 single-flight 刷新,带超时、冷却和 ETag;刷新后仍找不到就拒绝并短暂负缓存。随机 kid 不会放大成上游洪峰。JWKS 暂时故障时,已知密钥只在明示的有限陈旧窗口内继续使用,未知密钥始终 fail closed。
如果旧私钥泄露,我会停止旧签发、发布并切换新密钥,同时广播缓存失效并撤销旧 kid 或旧 iat 范围的令牌。这个流程不保留正常重叠窗口,因为攻击者可能正在签发。最后通过 JWKS 版本、缓存年龄、按 kid 的验签错误、新钥金丝雀和旧 kid 最后出现时间证明轮换完成,并定期演练过渡态和上游故障。”
常见错误
- 先切换签发器再发布公钥 → 缓存旧 JWKS 的服务无法验证新令牌 → 先发布、确认传播,再开始新签发。
- 新 JWKS 只放新公钥 → 尚未过期的旧令牌立即失效 → 在接受窗口内同时发布新旧公钥。
- 固定等待一个拍脑袋的时间 → 可能小于真实缓存或令牌寿命 → 从最大缓存陈旧时间、最后旧签发时间、令牌寿命和时钟偏差推导门禁。
- 每次请求都下载 JWKS → 发行方故障会拖垮所有认证并放大流量 → 本地缓存、条件刷新和有限陈旧策略。
- 每个未知 kid 都立即刷新 → 随机
kid可制造刷新风暴 → single-flight、限速、负缓存和输入上限。 - 未知 kid 时尝试所有密钥 → 模糊密钥选择规则并增加攻击面 → 刷新一次,仍无精确匹配就拒绝。
- 信任令牌里的 alg 或 jku → 可能产生算法混淆或 SSRF → 算法和 JWKS 地址都由验证方配置固定。
- 只验签不校验声明 → 其他发行方、受众或过期令牌仍可能被接受 → 同时校验
iss、aud、exp和nbf。 - 复用 kid 但替换密钥材料 → 缓存无法判断同一标识已改变 → 每一代密钥使用全新标识。
- 把删除中心 JWKS 当成即时撤销 → 验证方仍可能持有旧缓存 → 提供显式缓存失效与令牌撤销方案。
- 泄露时继续正常重叠 → 攻击者可在整个重叠期伪造令牌 → 走独立应急流程并接受必要的重新认证。
追问及应对
追问一:旧公钥到底要保留多久?
从最后一个旧密钥令牌的签发时间开始,至少保留“最大接受寿命 + 时钟偏差”。本题是 15 + 1 = 16 分钟。若系统还允许队列延迟、离线签发或更长的隐含接受窗口,也要计入。移除前同时观察旧 kid 流量,避免配置事实与运行事实不一致。
追问二:未知 kid 为什么不能直接返回 401?
因为合法轮换后,新令牌可能先于本进程的自然缓存刷新到达。一次受控刷新可以消除这段窗口。刷新必须合并并发、限速且只访问可信地址;最新集合仍无该 kid 时再拒绝,兼顾兼容性和抗放大攻击。
追问三:JWKS 端点宕机时是否应该 fail open?
不能跳过签名验证。缓存中有已信任且仍处于明示陈旧窗口的匹配公钥时,可继续完成完整验证;未知密钥或超过窗口时拒绝。这个策略让短时控制面故障不进入每次数据面请求,同时保持可量化的信任上限。
追问四:删除泄露密钥后,为什么旧令牌还可能有效?
验证方可能仍持有 10 分钟旧缓存,而且自包含 JWT 没有中心查询。应急流程需要主动失效缓存,并按旧 kid、签发时间或令牌版本执行临时拒绝;高风险系统也可以使用内省或服务端会话获得更快撤销。
追问五:如何防止轮换引发刷新风暴?
预发布让多数进程在自然刷新中先拿到新钥;主动刷新可以分批并加抖动。真实 miss 到来时,每个发行方用 single-flight 合并并发请求,再配合冷却、ETag、超时和短负缓存。监控 JWKS 请求量与 unknown-kid 基数,异常时限流而不是增加重试。
追问六:怎样自动化切签门禁?
为 JWKS 分配版本,验证服务定期上报已加载版本和可识别 kid。发布控制器要求目标服务达到预设覆盖率,并让关键路径金丝雀令牌成功后才激活新私钥。任何新 kid 验证失败率上升都暂停或回滚切签,但保留已发布的新公钥不会破坏旧令牌。
追问七:第三方验证方无法上报就绪怎么办?
必须公开稳定的重叠合同:新公钥提前发布至少一个最大缓存周期,旧公钥保留到所有旧令牌过期,并提供合理的缓存头。发行方无法强制第三方刷新,因此兼容窗口、变更通知和金丝雀验证比内部确认更重要;紧急泄露时仍可能产生不可避免的风险窗口。