ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JWT 密钥轮换实战:lcobucci/jwt 的 SignedWithOneInSet 约束与向后兼容轮换方案详解

JWT 密钥轮换实战:lcobucci/jwt 的 SignedWithOneInSet 约束与向后兼容轮换方案详解 认证鉴权后端【免费下载链接】jwtA simple library to work with JSON Web Token and JSON Web Signature项目地址https://gitcode.com/gh_mirrors/jw/jwt点击查看免费下载密钥轮换Key Rotation是 JWT 签发系统必须面对的工程命题长期使用同一把密钥签名既会放大密钥泄露的破坏范围也限制了算法升级的空间。本文以lcobucci/jwt库为核心从为什么要轮换到如何无痛轮换展开完整演示从HS256迁移到BLAKE2B的实战过程并深入剖析SignedWithOneInSet与SignedWithUntilDate两个验证约束的源码实现原理。读完本文你将掌握一套可复制、可落地的向后兼容密钥轮换方案让旧密钥自然过期、新密钥平滑接管用户全程无感。什么是密钥轮换为什么要定期轮换密钥轮换Key Rotation本质上是定期将旧的加密密钥退役并用新密钥将其替换。在 lcobucci/jwt 所实现的 JWT/JWS 生态中这意味着用于给 Token 签名、以及用于校验 Token 签名的密钥都会在一个受控的时间窗口内进行新旧交替。对签发系统而言定期执行密钥轮换是行业标准操作industry standard其收益体现在三个方面限制同一把密钥签发的 Token 数量降低密码分析cryptanalysis攻击的成功率。攻击者掌握的有效签名样本越多越有机会从统计角度逼近密钥本身轮换等于主动给密码分析断粮。获得采纳其他算法或更强密钥的机会。比如从HS256升级到BLAKE2B或从 1024 位 RSA 密钥升级到 4096 位这都需要以轮换为载体的换钥仪式。限制密钥泄露compromised keys造成的破坏范围。即便某把密钥意外泄露只要它已退役攻击者也只能伪造过去的 Token而无法影响未来的签发体系。轮换的真正挑战硬切换Hard Cut会让旧 Token 全部失效轮换本身并不难难的是轮换完成之后的那段过渡期。想象一个典型场景应用在某天完成了密钥轮换签发逻辑立刻切换到新密钥。但此时所有仍在使用中的旧 Token——那些在轮换前签发、尚未过期、仍然有效的 Token——在硬切换hard cut模式下会立刻校验失败。原因很简单旧 Token 是用旧密钥签名的而验证端只认新密钥签名自然对不上。想象一下你恰恰是在某次密钥轮换之前刚刚登录的那个用户那么轮换完成后你几乎肯定会被迫重新登录一次。这体验相当糟糕对吧这正是密钥轮换最需要被设计而非执行的地方轮换必须对存量用户向后兼容让旧 Token 在自然过期之前依然可用。轮换前的基线用 HS256 签发与验证 Token在引入平滑轮换之前先建立一个基线场景应用使用对称算法HS256配合一把密钥签发 Token。签发端代码如下取自 docs/rotating-keys.md 的完整示例?php declare(strict_types1); namespace MyApp; require vendor/autoload.php; use DateTimeImmutable; use Lcobucci\Clock\FrozenClock; use Lcobucci\JWT\Builder; use Lcobucci\JWT\JwtFacade; use Lcobucci\JWT\Signer; use Lcobucci\JWT\Signer\Key\InMemory; // FrozenClock 用于把时间固定在某一点从而让后续验证能够稳定通过 $clock new FrozenClock(new DateTimeImmutable(2023-11-04 21:06:0100:00)); $token (new JwtFacade(clock: $clock))-issue( new Signer\Hmac\Sha256(), InMemory::plainText( a-very-long-and-secure-key-that-should-actually-be-something-else ), static fn (Builder $builder): Builder $builder -issuedBy(https://api.my-awesome-app.io) -permittedFor(https://client-app.io) );几个值得注意的细节JwtFacade会自动补齐三个时间声明。查看 JwtFacade.php 的issue()实现可以发现它会在回调之前自动写入iat签发时间、nbf生效时间和exp过期时间默认在当前时间上加 5 分钟这也是后文验证能通过的前提。FrozenClock来自lcobucci/clock用于把系统时钟冻结在一个确定的时间点让示例可复现。composer.json中lcobucci/clock被列为建议安装suggest的依赖见 composer.json。InMemory::plainText()直接以明文形式加载密钥内容密钥不允许为空字符串空密钥会抛出InvalidKeyProvided::cannotBeEmpty()见 InMemory.php。对应的解析与验证逻辑如下使用SignedWith约束校验必须是这把密钥、这个算法签的再用StrictValidAt约束校验iat/nbf/exp三个时间声明?php declare(strict_types1); namespace MyApp; require vendor/autoload.php; use DateTimeImmutable; use Lcobucci\Clock\FrozenClock; use Lcobucci\JWT\JwtFacade; use Lcobucci\JWT\Signer; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Validation\Constraint; // FrozenClock 用于把时间固定在某一点从而让后续验证能够稳定通过 $clock new FrozenClock(new DateTimeImmutable(2023-11-04 21:06:3500:00)); $validationConstraints [ new Constraint\SignedWith( new Signer\Hmac\Sha256(), InMemory::plainText( a-very-long-and-secure-key-that-should-actually-be-something-else ), ), new Constraint\StrictValidAt($clock), ]; $jwt ; // 例如从请求头中取出 $token (new JwtFacade())-parse($jwt, ...$validationConstraints);注意JwtFacade::parse()的签名约束它要求至少传入一个SignedWith签名验证和一个ValidAt时间验证约束其余约束通过变长参数传入见 JwtFacade.php。若想在本机验证这段逻辑可以用文档中给出的样例 Token为可读性已加入换行eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 .eyJpYXQiOjE2OTkxMzE5NjEsIm5iZiI6MTY5OTEzMTk2MSwiZXhwIjoxNjk5MTMyMjYxLCJpc3MiOiJ odHRwczovL2FwaS5teS1hd2Vzb21lLWFwcC5pbyIsImF1ZCI6Imh0dHBzOi8vY2xpZW50LWFwcC5pbyJ9 .IA9S0n8Q2O97lyR8KczVE8g-hxbbH6_TfJS-JWTQR4c平滑轮换实操从 HS256 无缝迁移到 BLAKE2B现在进入本文的核心场景假设我们要把签发算法升级为新的对称算法BLAKE2B同时不允许任何未过期的旧 Token 失效。第一步修改签发逻辑只是普通轮换签发端的改动非常直接——替换签名器与密钥即可。BLAKE2B的密钥是一段 Base64 编码的原始字节因此需要使用InMemory::base64Encoded()而非plainText()加载?php declare(strict_types1); namespace MyApp; require vendor/autoload.php; use DateTimeImmutable; use Lcobucci\Clock\FrozenClock; use Lcobucci\JWT\Builder; use Lcobucci\JWT\JwtFacade; use Lcobucci\JWT\Signer; use Lcobucci\JWT\Signer\Key\InMemory; // FrozenClock 用于把时间固定在某一点从而让后续验证能够稳定通过 $clock new FrozenClock(new DateTimeImmutable(2023-11-04 21:06:0100:00)); $token (new JwtFacade(clock: $clock))-issue( - new Signer\Hmac\Sha256(), new Signer\Blake2b(), - InMemory::plainText( - a-very-long-and-secure-key-that-should-actually-be-something-else InMemory::base64Encoded( GOu4rLyVCBxmxPsbniU68ojAja5PkRdvv7vNvBCqDQ ), static fn (Builder $builder): Builder $builder -issuedBy(https://api.my-awesome-app.io) -permittedFor(https://client-app.io) );这里补充两点源码层面的背景Blake2b签名器对密钥长度有硬性要求查看 Blake2b.php其MINIMUM_KEY_LENGTH_IN_BITS 256即密钥原始字节长度必须达到 32 字节256 位否则在签名阶段就会抛出InvalidKeyProvided::tooShort()。sign()内部基于sodium_crypto_generichash()实现verify()则使用常量时间比较函数hash_equals()防止时序攻击。InMemory::base64Encoded()内部会先解码再使用见 InMemory.php所以传入的必须是合法的 Base64 字符串。该代码签发出的新 Token 样例换行为可读性添加eyJ0eXAiOiJKV1QiLCJhbGciOiJCTEFLRTJCIn0 .eyJpYXQiOjE2OTkxMzE5NjEsIm5iZiI6MTY5OTEzMTk2MSwiZXhwIjoxNjk5MTMyMjYxLCJpc3Mi OiJodHRwczovL2FwaS5teS1hd2Vzb21lLWFwcC5pbyIsImF1ZCI6Imh0dHBzOi8vY2xpZW50LWFwc C5pbyJ9.bD67s8IXpAJiBTIZn1et_M5WSS7kfmuNiacNRz5lArQ到目前为止这与普通轮换没有任何区别。真正的关键在验证端。第二步修改验证逻辑向后兼容的关键验证端的改动是核心把单一的SignedWith约束替换为SignedWithOneInSet并在其内部按优先级嵌套多个SignedWithUntilDate约束。每个SignedWithUntilDate都对应一把有明确退役日期的密钥?php declare(strict_types1); namespace MyApp; require vendor/autoload.php; use DateTimeImmutable; use Lcobucci\Clock\FrozenClock; use Lcobucci\JWT\JwtFacade; use Lcobucci\JWT\Signer; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Validation\Constraint; // FrozenClock 用于把时间固定在某一点从而让后续验证能够稳定通过 $clock new FrozenClock(new DateTimeImmutable(2023-11-04 21:06:3500:00)); $validationConstraints [ - new Constraint\SignedWith( - new Signer\Hmac\Sha256(), - InMemory::plainText( - a-very-long-and-secure-key-that-should-actually-be-something-else - ), - ), new Constraint\SignedWithOneInSet( new Constraint\SignedWithUntilDate( new Signer\Blake2b(), InMemory::base64Encoded( GOu4rLyVCBxmxPsbniU68ojAja5PkRdvv7vNvBCqDQ ), new DateTimeImmutable(2025-12-31 23:59:5900:00), $clock, ), new Constraint\SignedWithUntilDate( new Signer\Hmac\Sha256(), InMemory::plainText( a-very-long-and-secure-key-that-should-actually-be-something-else ), new DateTimeImmutable(2023-12-31 23:59:5900:00), $clock, ), ), new Constraint\StrictValidAt($clock), ]; $jwt ; // 例如从请求头中取出 $token (new JwtFacade())-parse($jwt, ...$validationConstraints);完成上述改动后应用现在能够同时接受新旧两种密钥签发的未过期 Token新密钥BLAKE2B签发的 Token其验证约束有效期到2025-12-31 23:59:5900:00旧密钥HS256签发的 Token其验证约束自动在2023-12-31 23:59:5900:00到期——即使工程师忘记手动把旧密钥从清单里删除到了这个时间点旧密钥也会因为约束过期而自然失效无法再通过验证。也就是说密钥退役这件事被直接编码进了验证逻辑里而不是依赖运维人员记得去改代码。原理剖析三个约束如何协作实现平滑轮换平滑轮换的魔法来自SignedWithOneInSet、SignedWithUntilDate、SignedWith三个约束的层层委托。理解它们各自的职责才能正确配置自己的轮换方案。SignedWithOneInSet按优先级逐个尝试任一通过即放行查看 SignedWithOneInSet.php 的完整实现final readonly class SignedWithOneInSet implements SignedWithInterface { /** var arraySignedWithUntilDate */ private array $constraints; public function __construct(SignedWithUntilDate ...$constraints) { $this-constraints $constraints; } public function assert(Token $token): void { $errorMessage It was not possible to verify the signature of the token, reasons:; foreach ($this-constraints as $constraint) { try { $constraint-assert($token); return; } catch (ConstraintViolation $violation) { $errorMessage . PHP_EOL . - . $violation-getMessage(); } } throw ConstraintViolation::error($errorMessage, $this); } }它的核心语义是集合内任一约束验证通过即整体通过按构造时传入的顺序逐个执行assert()一旦某个约束成功就直接返回只有当所有约束都失败时才抛出聚合了全部失败原因的ConstraintViolation。注意SignedWithOneInSet的构造参数类型被限定为SignedWithUntilDateSignedWithUntilDate ...$constraints这意味着它天然只用于带过期时间的签名验证这一场景。SignedWithUntilDate带退役日期的签名验证查看 SignedWithUntilDate.php 的实现它内部做了两件事public function assert(Token $token): void { if ($this-validUntil $this-clock-now()) { throw ConstraintViolation::error( This constraint was only usable until . $this-validUntil-format(DateTimeInterface::RFC3339), $this, ); } $this-verifySignature-assert($token); }先做时间闸门如果当前时间已超过validUntil即约束的退役日期直接抛出ConstraintViolation根本不会去碰签名——这就是旧密钥到点自动失效的机制来源。其构造函数接受Signer、Signer\Key、DateTimeImmutable $validUntil三个必填参数ClockInterface $clock为可选参数不传时默认使用系统真实时钟见 SignedWithUntilDate.php。再委托真正的签名验证时间闸门通过后把验证工作委托给内部创建的SignedWith实例完成。SignedWith最底层的签名/算法/密钥三重校验查看 SignedWith.php底层的SignedWith依次完成Token 类型检查Token 必须是UnencryptedToken非加密的普通 JWT否则报错You should pass a plain token算法匹配检查Token 头部alg必须与签名器声明的algorithmId()一致否则报错Token signer mismatch签名校验调用$this-signer-verify()用指定密钥验证签名失败报错Token signature mismatch。从测试用例看行为约定仓库的单元测试进一步印证了上述协作逻辑见 SignedWithOneInSetTest.php当所有SignedWithUntilDate约束都失败时抛出的异常消息会聚合所有失败原因例如同时包含Token signature mismatch与This constraint was only usable until ...只要任意一个约束成功哪怕其他约束全部失败assert()就静默通过——这正是平滑轮换的验证基础。约束顺序为什么重要文档中特别强调了一条容易被忽略的规则SignedWithUntilDate约束在SignedWithOneInSet中的顺序是有意义的强烈建议把旧密钥放在列表末尾。原因结合源码很好理解SignedWithOneInSet会按顺序逐个尝试约束并且在第一个成功的约束处短路返回。把新密钥放在最前面意味着绝大多数新签发的 Token 在第一次尝试时就通过验证不需要遍历整张密钥列表验证效率最高旧密钥约束排在后面只有在新密钥验证失败时才会被触及扮演兜底角色顺序同时也是一种隐性的优先级声明最信任、最优先的密钥放最前。如果你的应用有不止两代密钥例如同时存在 v1 / v2 / v3 三代也应该按照最新 → 最旧的顺序排列并给每一代设置各自的退役日期。完整轮换方案的时间线设计把上述代码组合起来一个可落地的向后兼容轮换方案是这样的时间线阶段签发端验证端效果轮换前HS256 旧密钥SignedWith仅旧密钥全部 Token 用旧密钥轮换日BLAKE2B 新密钥SignedWithOneInSet新密钥在前、旧密钥在后新旧 Token 同时有效存量用户无感旧密钥退役日2023-12-31之后维持BLAKE2BSignedWithOneInSet中旧约束自动失效旧 Token 自然过期旧密钥即使留在清单里也无法通过验证新密钥最终退役日2025-12-31之后按需再轮换同样机制再次滚动循环往复可以看到一旦建立了验证约束内置退役日期的机制后续每一轮轮换都只需重复同一个模式签发端换新签名器/新密钥验证端在SignedWithOneInSet列表头部追加新约束并设置退役日期。实践建议与注意事项结合源码与文档最后给出几条实战层面的建议对称密钥轮换与非对称密钥同样适用本文示例用的是对称算法HS256→BLAKE2B但SignedWithOneInSet/SignedWithUntilDate对 RSA、ECDSA、EdDSA 等非对称算法同样有效它们都实现自Signer接口只需把InMemory::plainText()换成InMemory::file()加载 PEM 密钥文件见 InMemory.php。不要把密钥硬编码在代码里示例为演示需要使用了InMemory::plainText()与InMemory::base64Encoded()生产环境应改用InMemory::file()从受保护的路径加载密钥并配合SensitiveParameter属性在堆栈跟踪中隐藏密钥内容。注意StrictValidAt的角色签名验证约束只回答这是谁签的时间有效性由StrictValidAt严格校验iat/nbf/exp三个声明见 StrictValidAt.php负责。轮换示例中两者始终搭配使用缺一不可。若你的场景需要容忍时钟偏差可参考 LooseValidAt 或给StrictValidAt传入 leeway 参数。退役日期要留足缓冲旧密钥的validUntil应覆盖所有已签发 Token 的最大过期时间否则未过期的旧 Token 会提前失效。示例中旧密钥退役日2023-12-31明显晚于 Token 签发时间2023-11-04加上默认 5 分钟有效期正是这个道理。环境要求本仓库要求 PHP~8.4.0 || ~8.5.0并依赖ext-openssl、ext-sodium与psr/clock ^1.0见 composer.json使用Blake2b签名器依赖ext-sodium扩展部署前务必确认已启用。密钥轮换不是一次性的运维事件而应当被设计成应用代码的一部分。借助SignedWithOneInSet与SignedWithUntilDatelcobucci/jwt把旧密钥自动退役变成了可声明的、有序的、可测试的验证逻辑——这正是本文想传达的核心工程思想。赞分享认证鉴权后端【免费下载链接】jwtA simple library to work with JSON Web Token and JSON Web Signature项目地址https://gitcode.com/gh_mirrors/jw/jwt点击查看免费下载相关推荐CANN/catlass MLA算子示例MLA Example Readme Code Organization ├── 19_mla │ ├── CMakeLists.txt CMake build算子库人工智能深度学习高性能计算CANNAscendJWT签名密钥轮换自动化tymon/jwt-auth方案JWT签名密钥轮换自动化tymon/jwt auth方案 你是否曾因JWT签名密钥泄露而面临系统安全风险是否在手动轮换密钥时遭遇服务中断本文将详细介绍如何认证鉴权后端安全无缝升级MediaMTX中JWT认证密钥轮换机制全解析无缝升级MediaMTX中JWT认证密钥轮换机制全解析 在实时流媒体服务中安全认证是保护内容不被未授权访问的关键环节。JSON Web TokenJWT音视频后端上一篇Flux项目贡献指南如何参与Rust精化类型工具的开发下一篇如何快速开始使用Aryabhata-2.0-GGUF5步安装教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表