跳到主要内容
ecdsa.com
PHP

lcobucci/jwt

Luís Cobucci 开发的 lcobucci/jwt,Laravel Passport 和众多 PHP 框架底层的 JWT 库:每项检查都是显式的约束,没有约束时 assert() 拒绝运行,密钥长度会被强制检查——但不支持 RSA-PSS。
许可证
BSD-3-Clause
检查的版本
5.6.1 ·
源代码
github.com/lcobucci/jwt
文档
lcobucci-jwt.readthedocs.io/en/stable/

兼容性概览

JOSE 与 JWT

JWS 算法
HS256HS384HS512RS256RS384RS512ES256ES384ES512EdDSA

lcobucci/jwt 把解析、签名和校验分开:Configuration 保存签名器和密钥,parser()->parse() 读取令牌但不做任何检查,validator()->assert() 应用你传入的约束。不带约束调用 assert() 会抛出 NoConstraintsGiven,因此很难无意中写出什么都不检查的校验。

SignedWith 锁定签名器和密钥:一个 RS256 令牌和一个用 EC 密钥的 PEM 文本伪造的 HS256 令牌,都报错 Token signer mismatch。未签名的令牌甚至无法解析(The JWT string is missing the Signature part)。密钥长度在签名时就会被强制检查——ES256 拒绝了 P-384 密钥(The length of the provided key is different than 256 bits),HS256 拒绝了 40 位的密钥——因此根本不会生成不合规的令牌。

时间检查也是约束,因此只有列出时才会执行:一个一小时前就已过期的令牌,只用 SignedWith 检查时照样通过。LooseValidAt 会在 exp、nbf 和 iat 存在时检查它们,并接受了不含 exp 的令牌;StrictValidAt 要求三者都存在——不含 iat 的令牌报错 "Issued At" claim missing,含 iat 和 exp 但没有 nbf 的令牌报错 "Not Before" claim missing。两者都接受可选的容差时间间隔,默认为零。示例设置了全部三个声明,并在一次调用中断言签名、颁发者和严格有效性。

5.6.1 支持 256/384/512 的 HS、RS、ES 以及 EdDSA;没有 PS* 签名器,也没有 ES256K,因此颁发者使用 RSA-PSS 的令牌需要用 web-token 处理。ES256 签名是 64 字节的原始 r‖s,DER 签名会报错 Token signature mismatch;JWT 调试器能显示令牌使用的是哪种编码。

事实及其依据的检查

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

  • JWS 算法

    HS256HS384HS512RS256RS384RS512ES256ES384ES512EdDSA

    256/384/512 的 HS、RS、ES 以及 EdDSA 的签名与验证均成功;没有 RSA-PSS 或 ES256K 的签名器类。

    检查记录
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, EdDSA
    evidence jws-algorithms: PS256 signer class Lcobucci\JWT\Signer\Rsa\Pss\Sha256 exists: false
    evidence jws-algorithms: ES256K signer class Lcobucci\JWT\Signer\Ecdsa\Sha256K exists: false
  • ES256 签名编码

    raw

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

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

    rejected

    DER 编码的 ES256 签名会报错 Token signature mismatch。

    检查记录
    fact jws-der-signature = rejected
    evidence jws-der-signature: Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - Token signature mismatch
  • 算法允许列表

    required

    SignedWith 锁定签名器:用 ES256 签名器验证 RS256 令牌,报错 Token signer mismatch;不带约束调用 assert() 会抛出 NoConstraintsGiven。

    检查记录
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: RS256 token with SignedWith(ES256 signer, EC key): Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - Token signer mismatch
    evidence jwt-algorithm-allowlist: assert() with no constraints: Lcobucci\JWT\Validation\NoConstraintsGiven: No constraint given.
  • 未签名的令牌(alg none)

    rejected

    未签名的令牌无法解析:The JWT string is missing the Signature part。

    检查记录
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: parsing an unsigned token: Lcobucci\JWT\Token\InvalidTokenStructure: The JWT string is missing the Signature part
  • 把公钥用作 HMAC 密钥

    rejected

    以 PEM 文本为密钥的 HS256 令牌,未通过 ES256 签名器的 SignedWith 检查,报错 Token signer mismatch。

    检查记录
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: SignedWith(ES256 signer, EC key): Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - Token signer mismatch
  • 按 alg 检查曲线

    enforced

    ES256 签名器拒绝了 P-384 密钥:The length of the provided key is different than 256 bits, 384 bits provided。

    检查记录
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: ES256 signer with a P-384 key: Lcobucci\JWT\Signer\InvalidKeyProvided: The length of the provided key is different than 256 bits, 384 bits provided
  • 解码时校验声明

    no

    解析和 SignedWith 都不检查时间:一小时前就已过期的令牌照样通过。

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

    0s

    除非传入时间间隔,否则 LooseValidAt 和 StrictValidAt 都不留任何容差。

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

    no

    LooseValidAt 接受了不含 exp 的令牌;StrictValidAt 要求 iat、nbf 和 exp 都存在。

    检查记录
    fact jwt-exp-required = no
    evidence jwt-exp-required: LooseValidAt without exp: ok; StrictValidAt: Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - "Issued At" claim missing
    evidence jwt-exp-required: StrictValidAt with iat and exp but no nbf: Lcobucci\JWT\Validation\RequiredConstraintsViolated: The token violates some mandatory constraints, details: - "Not Before" claim missing
  • 过短的 HMAC 密钥

    rejected

    HS256 拒绝了 5 字节的密钥:Key provided is shorter than 256 bits, only 40 bits provided。

    检查记录
    fact jwt-hmac-short-secret = rejected
    evidence jwt-hmac-short-secret: 5-byte HS256 key: Lcobucci\JWT\Signer\InvalidKeyProvided: Key provided is shorter than 256 bits, only 40 bits provided

用法

签发 ES256 令牌,并以 30 秒容差断言签名、颁发者和严格有效性。
<?php
require 'vendor/autoload.php';

use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Validation\Constraint\{IssuedBy, SignedWith, StrictValidAt};

$key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
openssl_pkey_export($key, $privatePem);
$private = InMemory::plainText($privatePem);
$public = InMemory::plainText(openssl_pkey_get_details($key)['key']);

$config = Configuration::forAsymmetricSigner(new Sha256(), $private, $public);
$now = new DateTimeImmutable();
$token = $config->builder()->issuedBy('https://auth.example.com')->relatedTo('user-42')
    ->issuedAt($now)->canOnlyBeUsedAfter($now)->expiresAt($now->modify('+10 minutes'))
    ->getToken($config->signer(), $config->signingKey());

// Every check is an explicit constraint; StrictValidAt requires iat, nbf and exp.
$parsed = $config->parser()->parse($token->toString());
$config->validator()->assert($parsed,
    new SignedWith($config->signer(), $config->verificationKey()),
    new IssuedBy('https://auth.example.com'),
    new StrictValidAt(SystemClock::fromUTC(), new DateInterval('PT30S')));
echo 'sub: ', $parsed->claims()->get('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 lcobucci/jwt:5.6.1 lcobucci/clock:3.6.0
命令
php usage.php
输出必须匹配(正则表达式)
sub: user-42

库