Ошибка проверки сеанса в Magento 1 EE v 1.14.3.x (и CE 1.9.3.x)


18

Я слежу за магазином Magento с 400-500 посетителями и 40-50 заказами в день. Недавно система была обновлена ​​с Magento EE 1.14.2.4 до Magento EE 1.14.3.2, и я заметил некоторые странные исключения в журналах:

exception 'Mage_Core_Model_Session_Exception' in
/var/www/.../app/code/core/Mage/Core/Model/Session/Abstract/Varien.php:418

Я преследовал это исключение, и я знаю, что оно запускается, потому что следующий код проверки сеанса не в состоянии проверить сеанс:

class Mage_Core_Model_Session_Abstract_Varien extends Varien_Object
{
// ...
    protected function _validate()
    {
//    ...
        if ($this->useValidateSessionExpire()
            && isset($sessionData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP])
            && $sessionData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP] < time() ) {

Этот if-блок был добавлен в файл с последним выпуском от Magento. И это, по-видимому, тормозное изменение, подробности см. Ниже.

Исключение происходит довольно часто, например, дюжина раз в день. но я не могу воссоздать условия, которые приводят к исключению, если я буквально не верю в условие выше. Исключения чаще всего встречаются на страницах с описанием товара и на последнем этапе оформления одной страницы. Магазин является магазином b2b, пользователь должен войти в систему, чтобы увидеть страницу продукта или иметь возможность оформить заказ. Это означает, что пользователь перенаправляется на страницы входа в систему, когда сеанс аннулирован / истек срок действия. На данный момент для меня важнее исправить эту проблему во время оформления заказа.

Что происходит с точки зрения пользователя: пользователь заполняет корзину, переходит к оформлению заказа и достигает последнего шага, затем нажимает кнопку «отправить заказ», и ничего не происходит. За кулисами JS Magento выполняет запрос AJAX, и JS ожидает получить JSON обратно, но в случае возникновения этой ошибки возвращается HTML-код страницы входа, который не может быть проанализирован JavaScript, и он просто ничего не делает. Это очень запутанно для пользователей.

Ну, это не полный пользовательский сценарий, мы связались с пользователями, и они сказали нам, что они ждали несколько дней между заполнением корзины и отправкой заказа, что это точно означает, что трудно понять, потому что люди просто не помнят этого.

Время жизни сессии PHP - 350000 (~ 4 дня в секундах) Время жизни куки - 345600 (4 дня)

Вот актуальный вопрос: как я могу узнать, какое поведение пользователя приводит к исключению?

ОБНОВЛЕНИЕ До сих пор я знаю, что исключение происходит в следующих классах согласно сделанному запросу, для меня это ничего не значит, к сожалению.

/catalogsearch/result/?q=…    Mage_Core_Model_Session
/checkout/cart/               Mage_Core_Model_Session
/checkout/onepage/saveOrder/… Mage_Rss_Model_Session
/customer/account/loginPost/  Mage_Core_Model_Session
/customer/account/loginPost/  Mage_Reports_Model_Session
/customer/account/logout/     Mage_Reports_Model_Session
/catalog/product/view/…       Mage_Reports_Model_Session
/catalog/product/view/…       Mage_Tag_Model_Session

ОБНОВЛЕНИЕ 2 : сессии хранятся в файлах и очищаются сборщиком мусора сессий PHP, независимо от того, является ли это хорошим выбором или нет, выходит за рамки этого вопроса.


Связанный: maxchadwick.xyz/blog/…
— Симон

Ответы:


24

После некоторой продвинутой отладки, отслеживания сеанса и обдумывания всей этой магии я смог воспроизвести проблему и понять причину ее возникновения. Я подготовил небольшую временную иллюстрацию, вы можете увидеть ее ниже.

проблемное время

  • красный флаг - момент входа пользователя и создания сессии
  • Синий флаг - это момент, когда пользователь открывает страницу каталога, давайте предположим, что это страница категории, которая открыта.
  • зеленый флаг - это момент, когда пользователь отправляет заказ ( /sales/order/save/...запрос)

Вот как воспроизвести:

  1. Перед началом: Установите для тайм-аута сеанса PHP и тайм-аута файла cookie Magento значение 1440, которое является значением PHP по умолчанию.
  2. Убейте все свои куки или откройте вкладку инкогнито.
  3. Зайдите в магазин Magento и войдите в систему (см. Флаг 1)
  4. Просмотрите каталог и добавьте некоторые товары в корзину (Флаг 2).
  5. Пройдите проверку и отправьте заказ. Обратите внимание на то время, когда вы это сделали. (Флаг 3)
  6. Пройдите по каталогу и добавьте некоторые товары в корзину (Флаг 4)
  7. Продолжайте обновлять страницу корзины или просматривать страницы каталога так долго, чтобы истек тайм-аут, настроенный для файлов cookie magento (флаги 5-6). Обратите внимание, что время между флагом 7 и флагом 3 должно быть больше, чем время ожидания файла cookie.
  8. Пройдите проверку и отправьте заказ (Флаг 7). Подача заказа не удастся из-за исключения, описанного в моем вопросе выше.

Причина:

Существуют определенные сеансы, которые создаются только по заданным запросам, например, Mage_Rss_Model_Sessionсоздаются только во время фактической проверки, а не при просмотре каталога. В то же время отметка времени истечения сеанса устанавливается только тогда, когда был создан экземпляр сеанса. Это означает, что если между двумя извлечениями было достаточно времени, а сеанс не был прерван (поскольку пользователь вышел из системы или истек срок действия файла cookie), новый код Magento будет считать этот сеанс не прошедшим проверку и выдаст исключение, что звучит несколько странно для мне.

Как исправить:

Ну, у меня есть несколько вариантов:

  1. Подождите, пока Magento не отреагирует на это и не пересмотрит этот код.
  2. Удалить этот код тем временем.
  3. Попробуйте установить для тайм-аута Magento cookie значение 0, если это вариант для вас.

Как я это выяснил:

  1. Я начал с добавления следующего к исходному коду Mage_Core_Model_Session_Abstract_Varien

    Mage::log(
        sprintf(
            'useValidateSessionExpire fail "%s" "%d" "%d" "%s" "%s" "%s"',
            print_r($sessionData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP], 1),
            time(),
            $this->_time,
            get_class($this),
            session_name(),
            session_id()
        ),
        Zend_Log::DEBUG,
        'session-validation.log',
        true
    );

    это дало мне хорошее представление о затронутых классах и их взаимосвязи, а также о том, сколько сеансов истекло. Но это не объясняло, почему это происходит и какие действия пользователя приводят к проблеме.

  2. Тогда я начал думать о том, как я могу отследить все изменения в данных сеанса, и наткнулся на этот вопрос /superuser/368231/automatic-versioning-upon-file-change-modify-create-delete, который я решил дать попытка gitи incronкомбинация, но после того, как я реализовал это и протестировал в песочнице, я понял, что на производстве у меня будет очень мало свободного места.

  3. Я решил создать небольшой скрипт PHP, который будет декодировать данные сеанса и записывать журналы для каждой сессии. Этот скрипт был вызванincron

    <?php
    //log-session-data-change.php
    
    $sessionLogStoragePath = '/var/www/html/logged-session-storage/';
    
    $sessionFilePath = $argv[1];
    $sessionOperationType = $argv[2];
    $sessionFileName = basename($sessionFilePath);
    
    session_start();
    session_decode(file_get_contents($sessionFilePath));
    
    $logString = sprintf(
      '"%s","%s","%s",""' . PHP_EOL,
      date(DateTime::COOKIE),
      $sessionOperationType,
      $sessionFileName
    );
    
    if (file_exists($sessionFilePath)) {
      session_start();
      session_decode(file_get_contents($sessionFilePath));
    
      foreach ($_SESSION as $name => $data) {
        $value = '<empty>';
        if (isset($data['_session_validator_data']) && isset($data['_session_validator_data']['session_expire_timestamp'])) {
          $value = $data['_session_validator_data']['session_expire_timestamp'];
        }
        $logString .= sprintf(
          '"","","","%s","%s"' . PHP_EOL,
          $name,
          $value
        );
      }
    }
    
    file_put_contents($sessionLogStoragePath . $sessionFileName, $logString, FILE_APPEND);

    и вот соответствующая incrontabзапись

    /var/www/html/magento-doc-root/var/session IN_MODIFY,IN_CREATE,IN_DELETE,IN_MOVE /usr/bin/php /var/www/html/log-session-data-change.php $@/$# $%

    образец вывода

    "Wednesday, 05-Apr-2017 18:09:06 CEST","IN_MODIFY","sess_94rfglnua0phncmp98hbr3k524",""
    "","","","core","1491408665"
    "","","","customer_base","1491408665"
    "","","","catalog","1491408665"
    "","","","checkout","1491408665"
    "","","","reports","1491408494"
    "","","","store_default","1491408665"
    "","","","rss","1491408524"
    "","","","admin","1491408524"

PS:

Актуальные версии обоих

skin/frontend/enterprise/default/js/opcheckout.js 
src/skin/frontend/base/default/js/opcheckout.js

не могут обработать указанное выше исключение во время запроса AJAX. Они буквально ничего не отображают для пользователя, в то время как пользователь фактически выходит из системы!

PPS:

версии Magento CE 1.9.3.x также подвержены уязвимости, см. https://github.com/OpenMage/magento-mirror/blame/magento-1.9/app/code/core/Mage/Core/Model/Session/Abstract/ Varien.php

PPPS:

Когда я сказал: «Тем временем удалите этот код». Я имел ввиду исключение следующего блока

if ($this->useValidateSessionExpire()
    && isset($sessionData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP])
    && $sessionData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP] < time() ) {
    return false;
} else {
    $this->_data[self::VALIDATOR_KEY][self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP]
        = $validatorData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP];
}

Вы можете сделать это многими способами, в том числе:

  1. Просто удалив этот бит из файла
  2. Комментируя это
  3. Возвращаясь перед этим
  4. Создание $this->useValidateSessionExpire()возвращающие
  5. ...
  6. Это программирование - будь креативным;)

Я просто отключил, <Mage_Rss>и это решило проблему (временное исправление) и подал заявку с поддержкой magento.
— Дамодар Башял

1
@DamodarBashyal, пожалуйста, имейте в виду, что проблема не только влияет на оформление заказа. Это также влияет на страницы продукта, я считаю, что могут быть затронуты и некоторые другие страницы. Причина - различный набор объектов сеанса инициализируется при каждом действии контроллера magento. Я могу предоставить больше объяснений, если это необходимо.
— Антон Борицкий

У меня была проблема с API, при создании груза я получал ошибку. Чтение было в порядке, но проблема была с записью, пока она не была отключена. Спасибо за информацию.
— Дамодар Башял

9

6. Это программирование - будь креативным;)

Другой способ исправить это (и улучшить проверку сеанса)

ColinM @ https://github.com/OpenMage/magento-lts

Код сеанса в настоящее время хранит данные средства проверки сеанса в каждом пространстве имен, а также проверяет их каждый раз, когда пространство имен инициируется. Это плохо, потому что:

  1. Чрезвычайно неэффективное пространство для хранения сеанса. Данные валидатора часто составляют более 50% пространства, используемого пространством имен, и при наличии большого количества пространств имен это приводит к большому количеству отходов. Хранилище сессий может быть существенно сокращено с помощью этого патча и при использовании хранилища в памяти, такого как Redis или Memcached, которое имеет большое значение.
  2. Неэффективность вычислительных циклов, поскольку множественные пространства имен означают множественные проверки, и нет веских причин, по которым они могут отличаться друг от друга.
  3. Фактически создает ошибки, такие как # 394, где данные валидатора обновляются по некоторым запросам, но не по другим (поэтому они могут отличаться, но не должны). Я не проверял, но я верю, что это также решит эту проблему.
diff --git a/app/code/core/Mage/Core/Model/Session/Abstract/Varien.php b/app/code/core/Mage/Core/Model/Session/Abstract/Varien.php
index 45d736543..ea6b464f1 100644
--- a/app/code/core/Mage/Core/Model/Session/Abstract/Varien.php
+++ b/app/code/core/Mage/Core/Model/Session/Abstract/Varien.php
@@ -35,6 +35,9 @@ class Mage_Core_Model_Session_Abstract_Varien extends Varien_Object
     const VALIDATOR_SESSION_EXPIRE_TIMESTAMP    = 'session_expire_timestamp';
     const SECURE_COOKIE_CHECK_KEY               = '_secure_cookie_check';

+    /** @var bool Flag true if session validator data has already been evaluated */
+    protected static $isValidated = FALSE;
+
     /**
      * Map of session enabled hosts
      * @example array('host.name' => true)
@@ -406,16 +409,21 @@ public function getValidateHttpUserAgentSkip()
     /**
      * Validate session
      *
-     * @param string $namespace
+     * @throws Mage_Core_Model_Session_Exception
      * @return Mage_Core_Model_Session_Abstract_Varien
      */
     public function validate()
     {
-        if (!isset($this->_data[self::VALIDATOR_KEY])) {
-            $this->_data[self::VALIDATOR_KEY] = $this->getValidatorData();
+        // Backwards compatibility with legacy sessions (validator data stored per-namespace)
+        if (isset($this->_data[self::VALIDATOR_KEY])) {
+            $_SESSION[self::VALIDATOR_KEY] = $this->_data[self::VALIDATOR_KEY];
+            unset($this->_data[self::VALIDATOR_KEY]);
+        }
+        if (!isset($_SESSION[self::VALIDATOR_KEY])) {
+            $_SESSION[self::VALIDATOR_KEY] = $this->getValidatorData();
         }
         else {
-            if (!$this->_validate()) {
+            if ( ! self::$isValidated && ! $this->_validate()) {
                 $this->getCookie()->delete(session_name());
                 // throw core session exception
                 throw new Mage_Core_Model_Session_Exception('');
@@ -432,8 +440,9 @@ public function validate()
      */
     protected function _validate()
     {
-        $sessionData = $this->_data[self::VALIDATOR_KEY];
+        $sessionData = $_SESSION[self::VALIDATOR_KEY];
         $validatorData = $this->getValidatorData();
+        self::$isValidated = TRUE; // Only validate once since the validator data is the same for every namespace

         if ($this->useValidateRemoteAddr()
                 && $sessionData[self::VALIDATOR_REMOTE_ADDR_KEY] != $validatorData[self::VALIDATOR_REMOTE_ADDR_KEY]) {
@@ -444,10 +453,8 @@ protected function _validate()
             return false;
         }

-        $sessionValidateHttpXForwardedForKey = $sessionData[self::VALIDATOR_HTTP_X_FORVARDED_FOR_KEY];
-        $validatorValidateHttpXForwardedForKey = $validatorData[self::VALIDATOR_HTTP_X_FORVARDED_FOR_KEY];
         if ($this->useValidateHttpXForwardedFor()
-            && $sessionValidateHttpXForwardedForKey != $validatorValidateHttpXForwardedForKey ) {
+                && $sessionData[self::VALIDATOR_HTTP_X_FORVARDED_FOR_KEY] != $validatorData[self::VALIDATOR_HTTP_X_FORVARDED_FOR_KEY]) {
             return false;
         }
         if ($this->useValidateHttpUserAgent()

Источник: https://github.com/OpenMage/magento-lts/commit/de06e671c09b375605a956e100911396822e276a


Обновить:

Исправить web/session/use_http_x_forwarded_for optionотключенную опцию ... https://github.com/OpenMage/magento-lts/pull/457/commits/ec8128b4605e82406679c3cd81244ddf3878c379


1
это выглядит хорошо на самом деле, любой опыт использования этого в производстве?
— Антон Борицкий

@AntonBoritskiy Да, я использую это в производстве. Работает отлично.
— sv3n

sv3n Есть ли потенциальные недостатки этого метода решения?
— Вайшал Патель

@VaishalPatel, если есть потенциальные плохие стороны, я на самом деле их не вижу :) Я использую это на производстве, и это решило все проблемы с проверкой сеанса. Я бы не стал публиковать это, если у меня есть какие-либо вопросы, но если у вас есть сомнения, пожалуйста, спросите здесь: github.com/OpenMage/magento-lts/pull/406 . Может быть, некоторые из "плюсов" SE тоже найдут время, чтобы рассмотреть это?
— sv3n

Я положу на свое производство. В любом случае это прогресс в направлении решения.
— Вайшал Патель

1

Как вы храните сессии? (т.е. в var / session / или в БД, или используя другие механизмы кэширования, такие как Redis или Memcached)

Независимо от того, что вы используете, убедитесь, что у вас правильные разрешения на запись var/session/(обычно это 755 для dirs и 644 для файлов), или если вы используете Redis или Memcache, убедитесь, что ваши настройки подключения и тайм-аута подходят для тех, ,

В Inchoo есть хороший учебник для Redis: http://inchoo.net/magento/using-redis-cache-backend-and-session-storage-in-magento/

При использовании Memcache ознакомьтесь с этой статьей (она ссылается на v1.10, но не должна сильно отличаться): http://www.magestore.com/magento/magento-sessions-disappearing-with-memcache-turned-on.html

Кроме того, если вы используете что-то вроде Varnish, в прошлом были проблемы с сеансами, когда требовалось пробивать определенные страницы.

Наконец, если вы используете файловую систему для своих сессий, вы можете найти облегчение, просто переключив <session_save>узел в вашем local.xml«db» вместо «files».

Из этого <session_save><![CDATA[files]]></session_save>

К этому <session_save><![CDATA[db]]></session_save>


спасибо за подсказку - я должен был добавить информацию к вопросу о том, как на самом деле хранить сессии, я храню их в файлах. Я только что понял исходную проблему, я считаю, что ошибка Magento. Я заверну это и скоро
— выложу

Отлично! ... Мой ответ вообще помог с решением?
— gtr1971

не совсем - посмотри мой ответ
— Антон Борицкий

0

Деталь Антона Борицкого фантастическая. Но вместо исключения этого блока вы можете сделать локальную копию, чтобы не редактировать ядро ​​и переписать блок следующим образом:

if ($this->useValidateSessionExpire() ) {
    // If the VALIDATOR_SESSION_EXPIRE_TIMESTAMP key is not set, do it now
    if( !isset($sessionData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP]) ) {
        // $this->_data is a reference to the $_SESSION variable so it will be automatically modified
        $this->_data[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP] = time() + $this->getCookie()->getLifetime();
        return true;
    } elseif ( $sessionData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP] < time() ) {
        return false;
    }
} else {
    $this->_data[self::VALIDATOR_KEY][self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP]
        = $validatorData[self::VALIDATOR_SESSION_EXPIRE_TIMESTAMP];
}

Это гарантирует, что сравнение между time () и session_expire_timestamp выполняется только тогда, когда ключ существует, и что при обнаружении сеанса, у которого нет ключа (то есть до сеанса до 1.9.3), ключ добавляется.


Добавление локальной копии и переопределение, конечно, лучше, чем изменение основных файлов, мы внутренне поддерживаем список исправлений, которые автоматически применяются во время сборки проекта, потому что Magento недавно выпустила пару таких ошибок.
— Антон Борицкий

В то же время я не вижу, как ваше изменение исправляет исходную проблему, не могли бы добавить немного более расширенное объяснение?
— Антон Борицкий

Анто Борицкий, это хороший крик со списком.
— Вайшал Патель

Анто Борицкий, Новый ключ используется при проверке достоверности метки времени сеанса. $ sessionData поступает из $ this -> _ data [self :: VALIDATOR_KEY]; но ключ session_expire_timestamp добавляется в сеанс только с помощью $ this-> getValidatorData (); функция и хранится в $ this -> _ data [...] в конце вызова функции. Таким образом, проблема заключается в том, что в существующих сеансах этот ключ session_expire_timestamp недоступен.
— Вайшал Патель
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.