diff --git a/composer.json b/composer.json index db17d607c735eade2f4c89094c78eb18a3f87452..12f83c2ca8dac6f1b8290e5b65a130fd17bbdb13 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "test-mqtt": "tests/phpunit -c src/Components/mqtt/tests/phpunit.xml", "test-smarty": "tests/phpunit -c src/Components/smarty/tests/phpunit.xml", "test-components": [ - "(php -r \"exit(version_compare(PHP_VERSION, '8.0', '<') ? 0 : 1);\" && composer test-jwt) || echo 'skip jwt'", + "composer test-jwt", "composer test-queue", "composer test-amqp", "composer test-kafka", diff --git a/doc/components/httpserver/jwt.md b/doc/components/httpserver/jwt.md index 17ca3b3330b5cc346b06a2561206ccb30144d3b1..0e405e17231e72aeaeca827116d571a4afe1435a 100644 --- a/doc/components/httpserver/jwt.md +++ b/doc/components/httpserver/jwt.md @@ -75,7 +75,7 @@ $data = [ 'memberId' => 19260817, ]; $token = JWT::getToken($data); // Token 对象 -$tokenContent = $token->__toString(); // Token 字符串 +$tokenContent = $token->toString(); // Token 字符串 ``` 指定名称: @@ -87,7 +87,7 @@ $data = [ 'memberId' => 19260817, ]; $token = JWT::getToken($data, 'a'); // Token 对象 -$tokenContent = $token->__toString(); // Token 字符串 +$tokenContent = $token->toString(); // Token 字符串 ``` 自定义处理: @@ -102,7 +102,7 @@ $token = JWT::getToken($data, 'a', function(\Lcobucci\JWT\Builder $builder){ // 可以针对该对象做一些操作 $builder->withClaim('aaa', 'bbb'); }); // Token 对象 -$tokenContent = $token->__toString(); // Token 字符串 +$tokenContent = $token->toString(); // Token 字符串 ``` ### 验证 Token @@ -114,7 +114,8 @@ use \Imi\JWT\Facade\JWT; /** @var \Lcobucci\JWT\Token $token */ $token = JWT::parseToken($jwt); // 仅验证是否合法 // $token = JWT::parseToken($jwt, 'a'); // 指定配置名称 -$data = $token->getClaim('data'); // 获取往token里丢的数据 +$data = $token->getClaim('data'); // 获取往token里丢的数据,PHP <= 7.3 +$data = $token->claim()->get('data'); // 获取往token里丢的数据,PHP >= 7.4 // 验证有效期、id、issuer、audience、subject $validationData = new \Lcobucci\JWT\ValidationData; diff --git a/src/Components/jwt/composer.json b/src/Components/jwt/composer.json index e2a1d4237dbef1d8fdfba69d487fc64d84c80189..35f7d6aa3b16ff8d09cf203a90a6765bc7324d1a 100644 --- a/src/Components/jwt/composer.json +++ b/src/Components/jwt/composer.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "在 imi 框架中非常方便地接入 jwt", "require": { - "lcobucci/jwt": "3.3.*" + "lcobucci/jwt": "^3.4.0|^4.1.0" }, "require-dev": {}, "autoload": { diff --git a/src/Components/jwt/src/Aop/JWTValidationAop.php b/src/Components/jwt/src/Aop/JWTValidationAop.php index 57bec9e947aa2213cfdca74b38f12b3c318a5618..43edd3cdfaf7e2405cda2d3fb6e1b90029880acc 100644 --- a/src/Components/jwt/src/Aop/JWTValidationAop.php +++ b/src/Components/jwt/src/Aop/JWTValidationAop.php @@ -17,6 +17,14 @@ use Imi\JWT\Exception\InvalidTokenException; use Imi\RequestContext; use Imi\Util\ClassObject; use Imi\Util\Http\Consts\RequestHeader; +use Lcobucci\Clock\FrozenClock; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Validation\Constraint\IdentifiedBy; +use Lcobucci\JWT\Validation\Constraint\IssuedBy; +use Lcobucci\JWT\Validation\Constraint\LooseValidAt; +use Lcobucci\JWT\Validation\Constraint\PermittedFor; +use Lcobucci\JWT\Validation\Constraint\RelatedTo; /** * @Aspect @@ -73,41 +81,84 @@ class JWTValidationAop $token = $jwt->parseToken($jwtStr, $jwtValidation->name ?? $jwt->getDefault()); // 验证 - $validationData = new \Lcobucci\JWT\ValidationData(); - if (false !== $jwtValidation->id) + if (3 === $jwt->getJwtPackageVersion()) { - $validationData->setId($jwtValidation->id ?? $config->getId()); - } - if (false !== $jwtValidation->issuer) - { - $validationData->setIssuer($jwtValidation->issuer ?? $config->getIssuer()); - } - if (false !== $jwtValidation->audience) - { - $validationData->setAudience($jwtValidation->audience ?? $config->getAudience()); - } - if (false !== $jwtValidation->subject) - { - $validationData->setSubject($jwtValidation->subject ?? $config->getSubject()); - } - if (!$token->validate($validationData)) - { - throw new InvalidTokenException(); + $validationData = new \Lcobucci\JWT\ValidationData(); + if (false !== $jwtValidation->id) + { + $validationData->setId($jwtValidation->id ?? $config->getId()); + } + if (false !== $jwtValidation->issuer) + { + $validationData->setIssuer($jwtValidation->issuer ?? $config->getIssuer()); + } + if (false !== $jwtValidation->audience) + { + $validationData->setAudience($jwtValidation->audience ?? $config->getAudience()); + } + if (false !== $jwtValidation->subject) + { + $validationData->setSubject($jwtValidation->subject ?? $config->getSubject()); + } + if (!$token->validate($validationData)) + { + throw new InvalidTokenException(); + } + if ($jwtValidation->tokenParam || $jwtValidation->dataParam) + { + $args = ClassObject::convertArgsToKV($class, $joinPoint->getMethod(), $joinPoint->getArgs()); + if ($jwtValidation->tokenParam) + { + $args[$jwtValidation->tokenParam] = $token; + } + if ($jwtValidation->dataParam) + { + $data = $token->getClaim($config->getDataName()); + $args[$jwtValidation->dataParam] = $data; + } + $args = array_values($args); + } } - - if ($jwtValidation->tokenParam || $jwtValidation->dataParam) + else { - $args = ClassObject::convertArgsToKV($class, $joinPoint->getMethod(), $joinPoint->getArgs()); - if ($jwtValidation->tokenParam) + $config = $jwt->getConfig($jwtValidation->name); + $configuration = Configuration::forAsymmetricSigner($config->getSignerInstance(), InMemory::plainText($config->getPrivateKey()), InMemory::plainText($config->getPublicKey())); + $constraints = []; + if (false !== $jwtValidation->id && null !== ($id = ($jwtValidation->id ?? $config->getId()))) + { + $constraints[] = new IdentifiedBy($id); + } + if (false !== $jwtValidation->issuer && null !== ($issuer = ($jwtValidation->issuer ?? $config->getIssuer()))) + { + $constraints[] = new IssuedBy($issuer); + } + if (false !== $jwtValidation->audience && null !== ($audience = ($jwtValidation->audience ?? $config->getAudience()))) + { + $constraints[] = new PermittedFor($audience); + } + if (false !== $jwtValidation->subject && null !== ($subject = ($jwtValidation->subject ?? $config->getSubject()))) + { + $constraints[] = new RelatedTo($subject); + } + $constraints[] = new LooseValidAt(new FrozenClock(new \DateTimeImmutable())); + if ($constraints && !$configuration->validator()->validate($token, ...$constraints)) { - $args[$jwtValidation->tokenParam] = $token; + throw new InvalidTokenException(); } - if ($jwtValidation->dataParam) + if ($jwtValidation->tokenParam || $jwtValidation->dataParam) { - $data = $token->getClaim($config->getDataName()); - $args[$jwtValidation->dataParam] = $data; + $args = ClassObject::convertArgsToKV($class, $joinPoint->getMethod(), $joinPoint->getArgs()); + if ($jwtValidation->tokenParam) + { + $args[$jwtValidation->tokenParam] = $token; + } + if ($jwtValidation->dataParam) + { + $data = $token->claims()->get($config->getDataName()); + $args[$jwtValidation->dataParam] = $data; + } + $args = array_values($args); } - $args = array_values($args); } return $joinPoint->proceed($args ?? null); diff --git a/src/Components/jwt/src/Bean/JWT.php b/src/Components/jwt/src/Bean/JWT.php index bcd26d1f0239a78d6b7a6641cafc065f0e173383..a23e5f2cef3dada6b86e3c37ccd022979df25a77 100644 --- a/src/Components/jwt/src/Bean/JWT.php +++ b/src/Components/jwt/src/Bean/JWT.php @@ -6,9 +6,12 @@ use Imi\Bean\Annotation\Bean; use Imi\JWT\Exception\ConfigNotFoundException; use Imi\JWT\Exception\InvalidTokenException; use Imi\JWT\Model\JWTConfig; -use Imi\JWT\Util\Builder; -use Imi\JWT\Util\Parser; +use Lcobucci\JWT\Builder; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Parser; +use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Token; +use Lcobucci\JWT\Validation\Constraint\SignedWith; /** * @Bean("JWT") @@ -104,37 +107,84 @@ class JWT { throw new ConfigNotFoundException('Must option the config @app.beans.JWT.list'); } - $builder = new Builder(); - $time = time(); - $builder->permittedFor($config->getAudience()) + if (3 === $this->getJwtPackageVersion()) + { + $builder = new Builder(); + $time = time(); + $builder->permittedFor($config->getAudience()) ->relatedTo($config->getSubject()) ->expiresAt($time + $config->getExpires()) ->issuedBy($config->getIssuer()) ->canOnlyBeUsedAfter($config->getNotBefore()) ->identifiedBy($config->getId()); - $issuedAt = $config->getIssuedAt(); - if (true === $issuedAt) - { - $builder->issuedAt($time); - } - elseif (false !== $issuedAt) - { - $builder->issuedAt($issuedAt); + $issuedAt = $config->getIssuedAt(); + if (true === $issuedAt) + { + $builder->issuedAt($time); + } + elseif (false !== $issuedAt) + { + $builder->issuedAt($issuedAt); + } + if ($headers = $config->getHeaders()) + { + foreach ($headers as $k => $v) + { + $builder->withHeader($k, $v); + } + } + $signer = $config->getSignerInstance(); + $key = $config->getPrivateKey(); + $builder->sign($signer, $key); } - if ($headers = $config->getHeaders()) + else { - foreach ($headers as $k => $v) + $configuration = Configuration::forAsymmetricSigner($config->getSignerInstance(), InMemory::plainText($config->getPrivateKey()), InMemory::plainText($config->getPublicKey())); + $builder = $configuration->builder(); + + $now = new \DateTimeImmutable(); + $builder->permittedFor($config->getAudience()) + ->relatedTo($config->getSubject()) + ->expiresAt($now->modify('+' . ($config->getExpires() ?? 0) . ' second')) + ->issuedBy($config->getIssuer()) + ->canOnlyBeUsedAfter($now->modify('+' . $config->getNotBefore() . ' second')) + ->identifiedBy($config->getId() ?? ''); + $issuedAt = $config->getIssuedAt(); + if (true === $issuedAt) { - $builder->withHeader($k, $v); + $builder->issuedAt($now); + } + elseif (false !== $issuedAt) + { + $builder->issuedAt($issuedAt); + } + if ($headers = $config->getHeaders()) + { + foreach ($headers as $k => $v) + { + $builder->withHeader($k, $v); + } } } - $signer = $config->getSignerInstance(); - $key = $config->getPrivateKey(); - $builder->sign($signer, $key); return $builder; } + public function getParserInstance(?string $name = null): Parser + { + if (3 === $this->getJwtPackageVersion()) + { + return new \Lcobucci\JWT\Parser(); + } + else + { + $config = $this->getConfig($name); + $configuration = Configuration::forAsymmetricSigner($config->getSignerInstance(), InMemory::plainText($config->getPrivateKey()), InMemory::plainText($config->getPublicKey())); + + return $configuration->parser(); + } + } + /** * 生成 Token. * @@ -142,7 +192,7 @@ class JWT * @param string|null $name * @param callable|null $beforeGetToken * - * @return \Lcobucci\JWT\Token + * @return \Lcobucci\JWT\Token|\Lcobucci\JWT\UnencryptedToken */ public function getToken($data, ?string $name = null, ?callable $beforeGetToken = null): Token { @@ -154,7 +204,14 @@ class JWT $config = $this->getConfig($name); $builder->withClaim($config->getDataName(), $data); - return $builder->getToken(); + if (3 === $this->getJwtPackageVersion()) + { + return $builder->getToken(); + } + else + { + return $builder->getToken($config->getSignerInstance(), InMemory::plainText($config->getPrivateKey())); + } } /** @@ -163,14 +220,18 @@ class JWT * @param string $jwt * @param string|null $name * - * @return \Lcobucci\JWT\Token + * @return \Lcobucci\JWT\Token|\Lcobucci\JWT\UnencryptedToken */ public function parseToken(string $jwt, ?string $name = null): Token { - $token = (new Parser())->parse($jwt); $config = $this->getConfig($name); - if ($config) + if (!$config) + { + throw new InvalidTokenException(); + } + if (3 === $this->getJwtPackageVersion()) { + $token = (new \Lcobucci\JWT\Parser())->parse($jwt); $signer = $config->getSignerInstance(); $key = $config->getPublicKey(); if (!$token->verify($signer, $key)) @@ -178,7 +239,28 @@ class JWT throw new InvalidTokenException(); } } + else + { + $parser = $this->getParserInstance($name); + $token = $parser->parse($jwt); + $signer = $config->getSignerInstance(); + $key = $config->getPublicKey(); + $signedWith = new SignedWith($signer, InMemory::plainText($key)); + try + { + $signedWith->assert($token); + } + catch (\Throwable $th) + { + throw new InvalidTokenException($th->getMessage(), $th->getCode(), $th->getPrevious()); + } + } return $token; } + + public function getJwtPackageVersion(): int + { + return class_exists(\Lcobucci\JWT\Token\Parser::class) ? 4 : 3; + } } diff --git a/src/Components/jwt/src/Facade/JWT.php b/src/Components/jwt/src/Facade/JWT.php index 76f5544a0c9b7a8b3baa4ca028184a979bbcc9b6..f5f2beacdadc1cffc1618e139bd1f95055f8589a 100644 --- a/src/Components/jwt/src/Facade/JWT.php +++ b/src/Components/jwt/src/Facade/JWT.php @@ -13,9 +13,10 @@ use Imi\Facade\BaseFacade; * @method static string|null getDefault() * @method static \Imi\JWT\Model\JWTConfig|null getConfig(string|null $name = NULL) * @method static \Lcobucci\JWT\Builder getBuilderInstance(string|null $name = NULL) - * @method static \Lcobucci\JWT\Token getToken(mixed $data, string|null $name = NULL, callable|null $beforeGetToken = NULL) - * @method static \Imi\JWT\Util\Parser getParserInstance(string|null $name = NULL) - * @method static \Lcobucci\JWT\Token parseToken(string $jwt, string|null $name = NULL) + * @method static \Lcobucci\JWT\Token|\Lcobucci\JWT\UnencryptedToken getToken(mixed $data, string|null $name = NULL, callable|null $beforeGetToken = NULL) + * @method static \Lcobucci\JWT\Parser getParserInstance(string|null $name = NULL) + * @method static \Lcobucci\JWT\Token|\Lcobucci\JWT\UnencryptedToken parseToken(string $jwt, string|null $name = NULL) + * @method static int getJwtPackageVersion() */ abstract class JWT extends BaseFacade { diff --git a/src/Components/jwt/src/Model/JWTConfig.php b/src/Components/jwt/src/Model/JWTConfig.php index d9b8a4706cdc316654edfcf74eaca213e8a47cd1..9f4417d1c3e9de4333fa91e13db0b7fa73b0ad94 100644 --- a/src/Components/jwt/src/Model/JWTConfig.php +++ b/src/Components/jwt/src/Model/JWTConfig.php @@ -66,7 +66,7 @@ class JWTConfig * * @var int */ - private $notBefore; + private $notBefore = 0; /** * JWT 发出时间 @@ -238,7 +238,7 @@ class JWTConfig * * @return string */ - public function getDataName() + public function getDataName(): string { return $this->dataName; } @@ -248,7 +248,7 @@ class JWTConfig * * @return int */ - public function getNotBefore() + public function getNotBefore(): int { return $this->notBefore; } diff --git a/src/Components/jwt/src/Util/Builder.php b/src/Components/jwt/src/Util/Builder.php deleted file mode 100644 index 6c88e3719e81ffba146480d038fba4d96ad23fc2..0000000000000000000000000000000000000000 --- a/src/Components/jwt/src/Util/Builder.php +++ /dev/null @@ -1,7 +0,0 @@ - 19260817, ]; $token = JWT::getToken($data, null, function (Builder $builder) { - $builder->expiresAt(strtotime('1926-08-17')); + if (3 === JWT::getJwtPackageVersion()) + { + $builder->expiresAt(strtotime('1926-08-17')); + } + else + { + $builder->expiresAt(new \DateTimeImmutable('1926-08-17')); + } }); - $tokenStr = (string) $token; + $tokenStr = $token->toString(); $token2 = JWT::parseToken($tokenStr); $config = JWT::getConfig(); - $this->assertEquals(json_encode($data), json_encode($token2->getClaim($config->getDataName()))); + if (3 === JWT::getJwtPackageVersion()) + { + $this->assertEquals(json_encode($data), json_encode($token2->getClaim($config->getDataName()))); + } + else + { + $this->assertEquals(json_encode($data), json_encode($token2->claims()->get($config->getDataName()))); + } } /**