跳到主要内容
ecdsa.com
PHP

web-token (JWT Framework)

Florent Morselli 开发的 JWT Framework(web-token),面向 PHP 和 Symfony 的完整 JOSE 实现:AlgorithmManager 就是允许列表,声明由单独的检查器检查,支持 RFC 7518 的全部签名算法以及 EdDSA——但不支持 ES256K。
许可证
MIT
检查的版本
4.2.3 ·
源代码
github.com/web-token/jwt-framework
文档
web-token.spomky-labs.com/

兼容性概览

JOSE 与 JWT

JWS 算法
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

web-token 把 JWS、JWE、JWK 和 JWK 集实现为相互独立的组件;web-token/jwt-library 将它们打包在一起,配套的 Symfony Bundle 则把它们注册为服务。需要 JWE、JWK 集管理或完整的算法列表时,它是 PHP 中的首选:4.2.3 版对 256/384/512 的 HS、RS、PS、ES 以及 EdDSA 的签名与验证均成功,其中包括 firebase/php-jwt 所缺少的 PS384/512 和 ES512。没有 ES256K 或完全指定的 Ed25519 算法类。

算法必须显式允许:传给 JWSVerifier 的 AlgorithmManager 就是允许列表,使用其他任何算法的令牌都会报错 The algorithm "RS256" is not supported.。none 作为算法类存在,但只有你把它放进管理器时才会被接受。用 EC 密钥的 PEM 文本伪造的 HS256 令牌,即使允许了 HS256 也验证失败,因为 EC JWK 不能用作 HMAC 密钥。用 P-384 密钥执行 ES256 得到的签名,被该库自己的验证器拒绝了。

验证签名和检查声明是两个独立的步骤:对一个一小时前就已过期的令牌,JWSVerifier::verifyWithKey() 返回了 true。之后要运行 ClaimCheckerManager,列出必需的声明——否则 exp 不是必需的——并给 ExpirationTimeChecker 传入允许的时钟漂移;从第 4 版起,它还需要一个 PSR-20 时钟。示例展示了完整的流程。

ES256 签名是 64 字节的原始 r‖s,DER 签名会让 verifyWithKey() 返回 false;JWT 调试器能显示令牌携带的是哪一种。密钥始终是 JWK 对象;JWKFactory 可以从 PEM、证书或密钥参数构建它们,JWKS 生成器则生成相同格式的密钥集。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/web-token-jwt-framework/run.sh 于2026年10月20日在容器镜像 php:8.5.11-cli-trixie 中运行了 web-token (JWT Framework) 4.2.3;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • JWS 算法

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSA

    256/384/512 的 HS、RS、PS、ES 以及 EdDSA 的签名与验证均成功;没有 ES256K 或 Ed25519 算法类。

    检查记录
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA
    evidence jws-algorithms: ES256K: class Jose\Component\Signature\Algorithm\ES256K exists: false
    evidence jws-algorithms: Ed25519: class Jose\Component\Signature\Algorithm\Ed25519 exists: false
  • ES256 签名编码

    raw

    ES256 签名是 64 字节的原始 r‖s。

    检查记录
    fact jws-ecdsa-encoding = raw
  • JWS 中的 DER 签名

    rejected

    DER 编码的 ES256 签名会让 verifyWithKey() 返回 false。

    检查记录
    fact jws-der-signature = rejected
    evidence jws-der-signature: false
  • 算法允许列表

    required

    AlgorithmManager 就是允许列表:用只含 ES256 的管理器验证 RS256 令牌,报错 The algorithm "RS256" is not supported.

    检查记录
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: RS256 token with an AlgorithmManager holding only ES256: InvalidArgumentException: The algorithm "RS256" is not supported.
  • 未签名的令牌(alg none)

    opt-in

    除非把 None 算法类加入管理器,否则未签名的令牌会报错 The algorithm "none" is not supported.

    检查记录
    fact jwt-alg-none = opt-in
    evidence jwt-alg-none: with an ES256 AlgorithmManager: InvalidArgumentException: The algorithm "none" is not supported.; a None algorithm class exists: true
  • 把公钥用作 HMAC 密钥

    rejected

    在同时允许 ES256 和 HS256 的情况下,以 PEM 文本为密钥的 HS256 令牌用 EC JWK 验证失败。

    检查记录
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: AlgorithmManager [ES256, HS256] with the EC JWK: false
  • 按 alg 检查曲线

    enforced

    用 P-384 密钥执行 ES256 签名成功了,但该库的验证器拒绝了生成的签名。

    检查记录
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: ES256 with a P-384 key, signing: ok; verifying: false
  • 解码时校验声明

    no

    对一小时前就已过期的令牌,JWSVerifier::verifyWithKey() 返回了 true;声明由单独的 ClaimCheckerManager 检查。

    检查记录
    fact jwt-claims-validated-on-decode = no
    evidence jwt-claims-validated-on-decode: JWSVerifier on a token expired an hour ago: ok
  • 默认时钟偏差

    0s

    除非传入容差,否则 ExpirationTimeChecker 不容许任何时钟漂移:三秒前过期的令牌被拒绝。

    检查记录
    fact jwt-clock-skew = 0s
  • 是否要求过期时间声明

    no

    必需声明中没有 exp 时,缺少它的令牌照样通过;把它列入必需声明后,抛出 MissingMandatoryClaimException。

    检查记录
    fact jwt-exp-required = no
    evidence jwt-exp-required: ExpirationTimeChecker without exp: ok; with exp listed as mandatory: Jose\Component\Checker\MissingMandatoryClaimException: The following claims are mandatory: exp.

用法

通过 AlgorithmManager 允许列表以 ES256 签名,验证签名,然后用声明检查器检查 exp 和 iss。
<?php
require 'vendor/autoload.php';

use Jose\Component\Checker\{ClaimCheckerManager, ExpirationTimeChecker, IssuerChecker};
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\{JWSBuilder, JWSVerifier};
use Jose\Component\Signature\Serializer\CompactSerializer;

$jwk = JWKFactory::createECKey('P-256');
$algorithms = new AlgorithmManager([new ES256()]); // the allow-list
$payload = json_encode(['sub' => 'user-42', 'iss' => 'https://auth.example.com', 'exp' => time() + 600]);
$jws = (new JWSBuilder($algorithms))->create()->withPayload($payload)->addSignature($jwk, ['alg' => 'ES256'])->build();
$token = (new CompactSerializer())->serialize($jws, 0);

// The signature check does not look at claims: run the claim checkers as a second step.
$parsed = (new CompactSerializer())->unserialize($token);
$valid = (new JWSVerifier($algorithms))->verifyWithKey($parsed, $jwk->toPublic(), 0);
$clock = new class implements Psr\Clock\ClockInterface { public function now(): DateTimeImmutable { return new DateTimeImmutable(); } };
$claims = json_decode($parsed->getPayload(), true);
(new ClaimCheckerManager([new ExpirationTimeChecker($clock, 30), new IssuerChecker(['https://auth.example.com'])]))->check($claims, ['exp', 'iss']);
echo 'valid: ', var_export($valid, true), ', sub: ', $claims['sub'], "\n";

代码片段测试工具会在容器镜像 php:8.5.11-cli-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
curl -fsSL -o /tmp/composer.phar https://getcomposer.org/download/2.10.3/composer.phar && echo '7a2d379d5b8ffdaa028580ef26494c36d2feef4b178d3dd1473a4dbc5e17c8d6 /tmp/composer.phar' | sha256sum -c - >/dev/null && apt-get -qq update >/dev/null && apt-get -qq install -y unzip >/dev/null 2>&1 && COMPOSER_ALLOW_SUPERUSER=1 php /tmp/composer.phar require --quiet web-token/jwt-library:4.2.3
命令
php usage.php
输出必须匹配(正则表达式)
valid: true, sub: user-42

库