diff --git a/app/.env.example b/app/.env.example index 1b35983752ef0ee1a788f1fa5196bf5171eea22b..4f07268a5cf9f0d594638deeb0c36b75e3403d73 100644 --- a/app/.env.example +++ b/app/.env.example @@ -7,6 +7,7 @@ DATABASE_URL="postgresql://user:password@db:5432/symf?serverVersion=16&charset=u JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem JWT_PASSPHRASE= +JWT_TTL=3600 ###< lexik/jwt-authentication-bundle ### ###> symfony/mailer ### diff --git a/app/config/packages/lexik_jwt_authentication.yaml b/app/config/packages/lexik_jwt_authentication.yaml index dd7a3aa5057695d211934700ec13353408f44273..11fddbfd0f363ff9f55ab7847e25146afe575426 100644 --- a/app/config/packages/lexik_jwt_authentication.yaml +++ b/app/config/packages/lexik_jwt_authentication.yaml @@ -2,4 +2,4 @@ lexik_jwt_authentication: secret_key: '%env(resolve:JWT_SECRET_KEY)%' public_key: '%env(resolve:JWT_PUBLIC_KEY)%' pass_phrase: '%env(JWT_PASSPHRASE)%' - token_ttl: 3600 + token_ttl: 604800 diff --git a/app/config/services.yaml b/app/config/services.yaml index 957845cd6b3097275bd530f0fef942e11b7ba6bd..07da5f9b97603b8a5bf5187807adce92a1aeff25 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -9,6 +9,7 @@ parameters: from_email: '%env(MAILER_ADDRESS)%' # Директория сохранения файлов images_directory: '%kernel.project_dir%/public/uploads/user_images' + jwt_ttl: '%env(JWT_TTL)%' services: # default configuration for services in *this* file @@ -25,6 +26,10 @@ services: - '../src/Entity/' - '../src/Kernel.php' + App\Service\RedisToken: + arguments: + $ttl: '%jwt_ttl%' + App\Service\Action\Classes\SaveImage: arguments: $targetDirectory: '%images_directory%' @@ -74,6 +79,16 @@ services: tags: - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_expired, method: onJWTExpired } + acme_api.event.jwt_created_listener: + class: App\Listeners\JwtListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated } + + acme_api.event.jwt_decoded_listener: + class: App\Listeners\JwtListener + tags: + - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_decoded, method: onJWTDecoded } + gesdinet.jwtrefreshtoken.send_token: class: App\Listeners\JwtRefreshListener arguments: diff --git a/app/src/Listeners/JwtListener.php b/app/src/Listeners/JwtListener.php index 6e5e61f2640c846e006a0c1d4552dddcd80af790..d202bcbc6e56a14fffcb4297306063aa37aefa7d 100644 --- a/app/src/Listeners/JwtListener.php +++ b/app/src/Listeners/JwtListener.php @@ -3,17 +3,27 @@ namespace App\Listeners; use App\Entity\User; +use App\Service\RedisToken; use App\Service\Response\Classes\TokenResponse; use JsonException; use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent; use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent; +use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; +use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent; use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTExpiredEvent; use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent; use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent; +use Psr\Cache\InvalidArgumentException; use Symfony\Component\HttpFoundation\Response; class JwtListener { + public function __construct( + private RedisToken $redisToken, + ) + { + } + /** * @param AuthenticationSuccessEvent $event * @@ -95,5 +105,41 @@ class JwtListener $response->setStatusCode(Response::HTTP_FORBIDDEN); $event->setResponse($response->getResponse()); + + $this->redisToken->clearOld(); + } + + /** + * @param JWTCreatedEvent $event + * + * @return void + * + * @throws InvalidArgumentException + */ + public function onJWTCreated(JWTCreatedEvent $event): void + { + $payload = $event->getData(); + + $this->redisToken->set($payload['username']); + } + + /** + * @param JWTDecodedEvent $event + * + * @return void + * + * @throws InvalidArgumentException + */ + public function onJWTDecoded(JWTDecodedEvent $event): void + { + if ($event->isValid()) { + $payload = $event->getPayload(); + + if (!$this->redisToken->check($payload['username'])) { + $event->markAsInvalid(); + } + + $event->setPayload($payload); + } } } \ No newline at end of file diff --git a/app/src/Service/RedisToken.php b/app/src/Service/RedisToken.php new file mode 100644 index 0000000000000000000000000000000000000000..9c1b0a3f8f81a757353ab50dfc4455c648047750 --- /dev/null +++ b/app/src/Service/RedisToken.php @@ -0,0 +1,123 @@ +ttl = $ttl; + $this->redis = Redis::getInstance(); + } + + /** + * Установка времени создания токена + * + * @param string $username + * + * @return int + * + * @throws InvalidArgumentException + */ + public function set(string $username): int + { + $time = time(); + $this->redis->set($this->format($this->getId($username)), $time); + return $time; + } + + /** + * Получение времени + * + * @param string $username + * + * @return bool + * + * @throws InvalidArgumentException + */ + public function check(string $username): bool + { + $time = $this->redis->get($this->format($this->getId($username))); + + if (!$time) { + $time = $this->set($username); + } + + $time += $this->ttl; + + $now = time(); + + if ($now > $time) { + $this->delete($username); + return false; + } + + $this->set($username); + + return true; + } + + /** + * Удаление времени + * + * @param string $username + * + * @return void + */ + public function delete(string $username): void + { + if ($id = $this->getId($username)) { + $this->redis->delete($this->format($id)); + } + } + + /** + * Очистка прошедшего времени + * + * @return void + * + * @throws InvalidArgumentException + */ + public function clearOld(): void + { + $now = time(); + + $users = $this->doctrine->getManager()->getRepository(User::class)->findAll(); + foreach ($users as $user) { + $code = $this->format($user->getId()); + if ($this->redis->has($code)) { + $time = $this->redis->get($code); + + if ($now > $time) { + $this->redis->delete($code); + } + } + } + } + + private function format(int $id): string + { + return 'token_ttl_' . $id; + } + + private function getId(string $username): ?int + { + $user = $this->doctrine->getManager()->getRepository(User::class)->findOneBy(['email' => $username]); + if ($user) { + return $user->getId(); + } + + return null; + } +} \ No newline at end of file