PHP redis стирает сами данные - PullRequest
0 голосов
/ 19 июня 2020

У меня есть приложение, написанное на PHP 7.3, где я хочу использовать Redis для хранения сеанса, я не использую Redis в качестве обработчика сеанса и вообще не использую $_SESSION (я действительно пробовал, но проблема заключалась в то же самое, поэтому я его переделываю, но это не помогло)

Что я делаю прямо сейчас:

  • когда пользователь пытается войти в систему
    • пытается получить пользователя из базы данных по учетным данным
    • если учетные данные верны, я генерирую случайный UUID
    • сохраняю пользовательские данные в Redis в течение 1800 секунд, добавляя этот UUID к ключу Redis
    • сохраняя этот UUID в файлах cookie пользователя
  • После успешного входа в систему пользователь перенаправляется на защищенную часть сайта через SessionAuthenticationMiddleware и проверку промежуточного программного обеспечения:
    • если у пользователя есть готовка ie (если нет, это означает, что пользователь не вошел в систему)
    • с использованием этого значения cook ie для генерации ключа Redis и попытки получить пользователя из сеанса (если ключ не существует, это означает, что пользователь не вошел в систему)
    • если пользователь существует в Redis, я снова сбрасываю ttl до 1800 секунд
    • проверяю, являются ли данные, которые я получил от Redis, существующим пользователем (если нет, я удаляю их из Redis, и это также означает, что пользователь не вошел в систему)
    • если все правильно , пользователь перенаправлен на защищенную часть страницы
    • та же проверка для каждой страницы браузера refre sh

Проблема в том, что по какой-то причине все данные например, стирая из Redis, я вхожу в систему и обновляю страницу 10 раз, в 11-й раз Redis стирает все данные и приложение, которое меня выводит из системы. Это происходит случайно. Иногда я могу войти в систему на 5 минут, иногда на несколько секунд. Я добавляю записи журнала, чтобы поместить их в код, отвечающий за удаление данных из Redis, и он никогда не вызывается. Это происходит на AWS (я использую собственный Redis, установленный на экземпляре EC2), на моем локальном компьютере все работает нормально.

Не уверен, нужен ли код, но я поделюсь им:

LoginService - получение пользователя из базы данных и передача его в службу UserSession:


    public function login(string $email, string $password): void
    {
        $userData = $this->usersRepository->verifyCredentialsAndGetUser($email, $password);
        if (empty($userData)) {
            throw new Exception('Incorrect credentials provided');
        }
        $this->userSession->setUserLoginSession($userData);
    }

UserSession - организация сеанса и готовка ie хранилище для сохранения / получения / проверки пользовательского сеанса

class UserSession
{
    public const AUTH_KEY = 'SOME_KEY';
    /**
     * @var RedisSessionHandler
     */
    private $session;
    /**
     * @var AuthCookie
     */
    private $authCookie;

    public function __construct(RedisSessionHandler $session, AuthCookie $authCookie)
    {
        $this->session = $session;
        $this->authCookie = $authCookie;
    }

    public function setUserLoginSession(array $userData): void
    {
        $this->authCookie->generateUserCookie();
        $uuid = $this->authCookie->getUserCookie();
        $this->session->write(self::AUTH_SESSION_KEY . ':' . $uuid, json_encode($userData));
    }

    public function clearUserLoginSession(): void
    {
        $uuid = $this->authCookie->getUserCookie();
        $this->authCookie->clearUserCookie();
        $this->session->destroy(self::AUTH_SESSION_KEY . ':' . $uuid);
    }

    public function getCurrentUser(): ?array
    {
        if ($this->authCookie->getUserCookie() === false) {
            return null;
        }
        $uuid = $this->authCookie->getUserCookie();
        $userData =  $this->session->read(self::AUTH_SESSION_KEY . ':' . $uuid);
        if (!empty($userData)) {
            return json_decode($userData, true);
        }

        return null;
    }

    public function isAuthorized(): bool
    {
        if ($this->getCurrentUser() === null) {
            return false;
        }

        $uuid = $this->authCookie->getUserCookie();
        $authorized =  $this->session->read(self::AUTH_SESSION_KEY . ':' . $uuid);
        if (empty($authorized) || $authorized === false) {
            return false;
        }

        return true;
    }
}

AuthCook ie - оболочка для управления данными, связанными с аутентификацией пользователя, сохранение / получение / удаление из Cook ie

class AuthCookie
{
    public const AUTH_COOKIE_USER = '_user_session';

    private $ttl = 1800; // 30 minutes default
    /**
     * @var CookieInterface
     */
    private $cookie;

    public function __construct(CookieInterface $cookie)
    {
        $this->cookie = $cookie;
    }

    public function generateUserCookie(bool $forever = false): bool
    {
        $uuid = Uuid::uuid4();

        if ($forever === true) {
            return $this->cookie->forever(self::AUTH_COOKIE_USER, $uuid->toString());
        }
        return $this->cookie->set(self::AUTH_COOKIE_USER, $uuid->toString(), $this->ttl);
    }

    public function hasUserCookie(): bool
    {
        return $this->getUserCookie() !== null;
    }

    public function getUserCookie(): ?string
    {
        return $this->cookie->get(self::AUTH_COOKIE_USER);
    }

    public function clearUserCookie(): bool
    {
        return $this->cookie->delete(self::AUTH_COOKIE_USER);
    }
}

Cook ie - базовый повар ie класс для хранения повара ie любого вида

class Cookie implements CookieInterface
{
    /**
     * $_COOKIE global variable
     *
     * @var array
     */
    private $cookie;

    public function __construct()
    {
        $this->cookie = $_COOKIE;
    }

    public function get(string $name): ?string
    {
        if (!isset($this->cookie[$name])) {
            return null;
        }

        return strip_tags(stripslashes($this->cookie[$name]));
    }

    public function set(
        string $name,
        ?string $value,
        int $expire = 0,
        ?string $path = null,
        ?string $domain = null,
        ?bool $secure = false,
        ?bool $httpOnly = false
    ): bool {
        setcookie(
            $name,
            $value ?? '',
            $this->calculateExpirationTime($expire),
            $path ?? '',
            $domain ?? '',
            $secure ?? false,
            $httpOnly ?? false
        );
        $this->cookie[$name] = $value;

        return true;
    }

    public function forever(string $name, string $value): bool
    {
        $this->set( $name, $value, 31536000 * 5);
        $this->cookie[$name] = $value;
    }

    public function delete(string $name): bool
    {
        unset($this->cookie[$name]) ;
        return $this->set($name, null, time() - 15 * 60 );
    }

    private function calculateExpirationTime( $expire = 0 ): int
    {
        return (int)($expire > 0 ? time() + $expire : -1 );
    }
}

RedisSessionHandler - отвечает за манипуляции с данными, связанными с сессией сохранить / получить / удалить из Redis. В настоящее время я не устанавливаю его как обработчик сеанса PHP (session_set_save_handler()), но я попытался, и проблема была той же.

class RedisSessionHandler implements \SessionHandlerInterface
{
    private $ttl = 1800; // 30 minutes default
    private $db;
    private $prefix;
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger, \Redis $db, string $prefix = 'PHPSESSID:', int $ttl = 1800) {
        $this->db = $db;
        $this->prefix = $prefix;
        $this->ttl = $ttl;
        $this->logger = $logger;
    }

    public function open($savePath, $sessionName): bool
    {
        return true;
    }

    public function close(): bool
    {
        return true;
    }

    public function read($id) {
        $id = $this->getRedisKey($id);
        $this->logger->debug(
            sprintf(
                '%s Reading data for id %s',
                __CLASS__,
                $id
            )
        );
        $sessData = $this->db->get($id);
        $this->logger->debug(
            sprintf(
                '%s Result of reading data for id %s is: %s',
                __CLASS__,
                $id,
                $sessData
            )
        );
        if (empty($sessData)) {
            return '';
        }
        $this->logger->debug(
            sprintf(
                '%s time to leave for id %s is %s ',
                __CLASS__,
                $id,
                $this->db->ttl($id)
            )
        );
        $this->db->expire($id, $this->ttl);
        return $sessData;
    }

    public function write($id, $data) : bool
    {
        $id = $this->getRedisKey($id);
        $this->logger->debug(
            sprintf(
                '%s Writing data %s for id %s',
                __CLASS__,
                $data,
                $id
            )
        );
        $result = $this->db->set($id, $data, $this->ttl);
        $this->logger->debug(
            sprintf(
                '%s Write result for id %s is %s and expiration %s',
                __CLASS__,
                $id,
                $result === true ? 'true' : 'false',
                $this->ttl
            )
        );

        return true;
    }

    public function destroy($id): bool
    {
        $id = $this->getRedisKey($id);
       // this method never called, no log entiries written so a assume that application is not 
       // deleting redis key
        $this->logger->debug(
            sprintf(
                '%s Destroy id %s ',
                __CLASS__,
                $id
            )
        );
        $this->db->del($id);
        $this->close();
        return true;
    }

    public function gc($maxLifetime): bool
    {
        return true;
    }

    protected function getRedisKey($key)
    {
        if (empty($this->prefix)) {
            return $key;
        }
        return $this->prefix . $key;
    }
}

SessionAuthenticationMiddleware - отвечает за проверку того, вошел ли пользователь в систему или нет, и перенаправляет в соответствующее место.

final class SessionAuthenticationMiddleware implements MiddlewareInterface
{
    /**
     * @var UserSession
     */
    private $userSession;
    /**
     * @var RouteCollectorInterface
     */
    private $routeCollector;
    /**
     * @var UserRepositoryInterface
     */
    private $userRepo;

    public function __construct(
        UserSession $session,
        RouteCollectorInterface $routeCollector,
        UserRepositoryInterface $userRepositoryInterface
    ) {
        $this->userSession = $session;
        $this->routeCollector = $routeCollector;
        $this-userRepo = userRepositoryInterface
    }

    /**
     * @inheritDoc
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        /** @var Route $currentRoute */
        $currentRoute = $request->getAttribute(RouteContext::ROUTE);
        $isAuthorized = $this->isAuthorised();
        // if route is guarded and user not authorised redirect to login
        if ($isAuthorized === false && $this->isGuardedRoute($currentRoute)) {
            return $this->notAuthorizedResponse($request);
        }
        // if route is not guarded but user is authorized redirect to admin dashboard,
        // also we need to check if provided user exist in our system,
        // if not destroy the session and redirect to login
        if ($isAuthorized && $this->isGuardedRoute($currentRoute) === false) {
            return $this->redirectToDashboard();
        }

        return $handler->handle($request);
    }

    private function isGuardedRoute(Route $route): bool
    {
        return in_array($route->getName() , ['get.login', 'post.login'], true) === false;
    }

    private function isAuthorised(): bool
    {
        if (!$this->userSession->isAuthorized()) {
            return false;
        }

        $userSession = $this->userSession->getCurrentUser();
        if (empty($userSession)) {
            return false;
        }
        try {
            $userExist=  $this-userRepo->getUser(
                Uuid::fromString(
                    $userSession['id']
                )
            );

            if ($userExist === false) {
                $this->userSession->clearUserLoginSession();
                return false;
            }
        } catch (ApplicationException $exception) {
            $this->logger->info($exception->getMessage(), ['exception' => $exception]);
            $this->userSession->clearUserLoginSession();
            return false;
        } catch (ApiException $exception) {
            $this->logger->warning($exception->getMessage(), ['exception' => $exception]);
            $this->userSession->clearUserLoginSession();
            return false;
        }

        return true;
    }

    private function notAuthorizedResponse(RequestInterface $request): Response
    {
        $response = new Response();
        if ($request->getHeaderLine('X-Requested-With') === 'xmlhttprequest') {
            $response->getBody()->write(
                json_encode(
                    [
                        'error' => [
                            'message' => 'Not authorized'
                        ],
                    ],
                    JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
                )
            );

            return $response->withHeader('Content-Type', 'application/json')
                ->withStatus(302);
        }

        return $response
            ->withHeader('Location', $this->routeCollector->getRouteParser()->urlFor('get.login'))
            ->withStatus(302);
    }

    private function redirectToDashboard(): Response
    {
        $response = new Response();
        return $response
            ->withHeader('Location', $this->routeCollector->getRouteParser()->urlFor('get.dashboard'))
            ->withStatus(302);
    }
}

Я уже часами смотрю, что не так, и думаю что-то может быть не так с самой конфигурацией Redis / PHP, но не в коде, но я не знаю, что мне нужно искать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...