roadrunner-bundle icon indicating copy to clipboard operation
roadrunner-bundle copied to clipboard

Problem with sessions

Open webspec2012 opened this issue 1 year ago • 4 comments

Good afternoon! Faced a problem with using sessions.

I use Symfony 6.2.

Sometimes it works fine, and sometimes the session is recreated every time. The problem does not reproduce if the application is running in DEV mode. And also if you run it in PROD mode, but set `http.pool.num_workers = 1'.

Changing kernel_reboot.strategy to always does not lead to anything.

webspec2012 avatar May 22 '23 09:05 webspec2012

Hello,

I tried on a fresh project, and it seems to be working with default configuration.

What version of this package and Symfony are you using?

Can you share your session config in config/packages/framework.yaml?

Baldinof avatar Jun 01 '23 07:06 Baldinof

Good afternoon!

Im use:

  • symfony/security-bundle 6.2.10
  • baldinof/roadrunner-bundle 3.0.0

Config:

    session:
        name: 'rtsid'
        handler_id: app.sessions.handler
        cookie_secure: auto
        cookie_samesite: lax

Services

    app.sessions.cache:
        class: \Symfony\Component\Cache\Adapter\RedisTagAwareAdapter
        arguments:
            $redis: '@app.sessions.redis'

    app.sessions.handler:
        class: App\FrontendWeb\Security\WebSession\WebSessionRedisHandler
        arguments:
            $webSessionCache: '@app.sessions.cache'

webspec2012 avatar Jun 01 '23 07:06 webspec2012

Is it possible to see the code in WebSessionRedisHandler?

Baldinof avatar Jun 01 '23 07:06 Baldinof

WebSessionRedisHandler

<?php
namespace App\FrontendWeb\Security\WebSession;

use Psr\Cache\CacheException;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler;
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;

/**
 * Web Session Redis Handler
 */
final class WebSessionRedisHandler extends AbstractSessionHandler
{
    /**
     * @var RedisTagAwareAdapter Web Session Cache
     */
    private RedisTagAwareAdapter $webSessionCache;

    /**
     * @var Security Security
     */
    private Security $security;

    /**
     * @var RequestStack Request Stack
     */
    private RequestStack $requestStack;

    /**
     * @var LoggerInterface Logger
     */
    private LoggerInterface $logger;

    /**
     * @var string Префикс redis ключей
     */
    private string $prefix;

    /**
     * @var int|null Время жизни в секундах
     */
    private ?int $ttl = null;

    /**
     * List of available options:
     *  * prefix: The prefix to use for the keys in order to avoid collision on the Redis server
     *  * ttl: The time to live in seconds.
     *
     * @param RedisTagAwareAdapter $webSessionCache Web Session Cache
     * @param Security $security Security
     * @param RequestStack $requestStack Request Stack
     * @param LoggerInterface $logger Logger
     * @param array $options Options
     *
     * @throws \InvalidArgumentException When unsupported client or options are passed
     */
    public function __construct(
        RedisTagAwareAdapter $webSessionCache,
        Security $security,
        RequestStack $requestStack,
        LoggerInterface $logger,
        array $options = [],
    )
    {
        if ($diff = \array_diff(\array_keys($options), ['prefix', 'ttl'])) {
            throw new \InvalidArgumentException(\sprintf('Указанные параметры не поддерживаются "%s".', \implode(', ', $diff)));
        }

        $this->webSessionCache = $webSessionCache;
        $this->security = $security;
        $this->requestStack = $requestStack;
        $this->logger = $logger;

        if (isset($options['prefix']) && \is_string($options['prefix'])) {
            $this->prefix = $options['prefix'];
        } else {
            $this->prefix = 'sf_s';
        }

        if (isset($options['ttl']) && \is_int($options['ttl'])) {
            $this->ttl = $options['ttl'];
        } else {
            $this->ttl = (int) \ini_get('session.gc_maxlifetime');
        }
    }

    /**
     * @throws InvalidArgumentException
     */
    protected function doRead(string $sessionId): string
    {
        $cacheItem = $this->webSessionCache->getItem($this->prefix.$sessionId);
        if ($cacheItem->isHit()) {
            return (string) $cacheItem->get();
        }

        return '';
    }

    /**
     * @throws InvalidArgumentException|CacheException В случае ошибки
     */
    protected function doWrite(string $sessionId, string $data): bool
    {
        return $this->saveData($sessionId, $data);
    }

    /**
     * @throws InvalidArgumentException|CacheException В случае ошибки
     */
    protected function doDestroy(string $sessionId): bool
    {
        return $this->webSessionCache->delete($this->prefix.$sessionId);
    }

    /**
     * {@inheritdoc}
     */
    #[\ReturnTypeWillChange]
    public function close(): bool
    {
        return true;
    }

    public function gc(int $maxlifetime): int|false
    {
        return 0;
    }

    /**
     * @throws InvalidArgumentException|CacheException В случае ошибки
     */
    public function updateTimestamp(string $sessionId, string $data): bool
    {
        return $this->saveData($sessionId, $data);
    }

    /**
     * @param string $sessionId Идентификатор сессии
     * @param string $data Данные сессии
     * @return bool Обновление данных сессии
     * @throws InvalidArgumentException|CacheException В случае ошибки
     */
    private function saveData(string $sessionId, string $data): bool
    {
        $cacheItem = $this->webSessionCache->getItem($this->prefix.$sessionId);
        $cacheItem->set($data);
        $cacheItem->expiresAfter($this->ttl);

        // tagged session
        $tags = [];

        if ($user = $this->security->getUser()) {
            $tags[] = 'user-'.$user->getUserIdentifier();
        }

        $token = $this->security->getToken();
        if ($token && $token->hasAttribute('usid')) {
            $tags[] = 'usid-'.(string) $token->getAttribute('usid');
        }

        $request = $this->requestStack->getMainRequest();
        if ($request && $usidt = $this->getUserSessionTokenId($request)) {
            $tags[] = 'usidt-'.\str_replace(['{','}','(',')','/','\\','@',':'], '_', $usidt);
        }

        if (!empty($tags)) {
            $cacheItem->tag($tags);
        }

        $this->logger->debug(\sprintf("%s: %s, tags: %s", __METHOD__, $this->prefix.$sessionId, \implode(', ', $tags)));

        return $this->webSessionCache->save($cacheItem);
    }

    /**
     * @param Request $request Request
     * @return string|null User Session Token Id
     */
    protected function getUserSessionTokenId(Request $request): ?string
    {
        if (!$rawCookie = $request->cookies->get('rmt')) {
            return null;
        }

        list($seriesId, ) = \explode(':', RememberMeDetails::fromRawCookie((string) $rawCookie)->getValue());
        return $seriesId;
    }
}

webspec2012 avatar Jun 01 '23 08:06 webspec2012