Ошибка проверки сеанса в 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

sv3n
источник
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>

gtr1971
источник
спасибо за подсказку - я должен был добавить информацию к вопросу о том, как на самом деле хранить сессии, я храню их в файлах. Я только что понял исходную проблему, я считаю, что ошибка 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 недоступен.
Вайшал Патель