Baldinof / roadrunner-bundle

A RoadRunner worker integrated in your Symfony app
MIT License
255 stars 46 forks source link

Problem with sessions #114

Open webspec2012 opened 1 year ago

webspec2012 commented 1 year ago

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.

Baldinof commented 1 year ago

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?

webspec2012 commented 1 year ago

Good afternoon!

Im use:

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'
Baldinof commented 1 year ago

Is it possible to see the code in WebSessionRedisHandler?

webspec2012 commented 1 year ago

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;
    }
}