Error de validación de sesión en Magento 1 EE v 1.14.3.x (y CE 1.9.3.x)

18

Estoy cuidando una tienda de Magento con 400-500 visitantes y 40-50 pedidos por día. Recientemente, el sistema se actualizó de Magento EE 1.14.2.4 a Magento EE 1.14.3.2 y noté algunas excepciones extrañas en los registros:

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

Estaba persiguiendo esa excepción y sé que se está disparando porque el siguiente código de validación de sesión no valida la sesión:

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() ) {

Este if-block se agregó al archivo con la última versión de Magento. Y aparentemente este es un cambio de frenado, vea más detalles a continuación.

La excepción ocurre con bastante frecuencia, como una docena de veces al día. pero no puedo recrear condiciones que conducen a la excepción, a menos que literalmente ponga verdadero en la condición anterior. Las excepciones ocurren con mayor frecuencia en las páginas de detalles del producto y en el último paso del pago de una página. La tienda es una tienda b2b, el usuario debe iniciar sesión para ver la página del producto o para poder pagar, significa que el usuario es redirigido a las páginas de inicio de sesión cuando la sesión se invalida / expira. Por el momento, es más importante para mí solucionar este problema durante el pago.

Lo que sucede desde la perspectiva del usuario: el usuario llena el carrito, procede al pago y llega al último paso, luego presiona el botón "enviar el pedido" y no sucede nada. Detrás de escena, JS de Magento realiza una solicitud AJAX y JS espera recibir JSON nuevamente, pero si ocurre este error, se devuelve el HTML de la página de inicio de sesión, que JavaScript no puede analizar y simplemente no hace nada. Eso es súper confuso para los usuarios.

Bueno, ese no es el escenario completo del usuario, contactamos a los usuarios y nos dijeron que esperaron unos días entre llenar el carrito y enviar el pedido, lo que eso significa exactamente es difícil de entender, porque la gente simplemente no lo recuerda.

Duración de la sesión PHP: 350000 (~ 4 días en segundos) Duración de la cookie: 345600 (4 días)

Aquí está la pregunta real: ¿cómo puedo averiguar qué tipo de comportamiento del usuario conduce a la excepción?

ACTUALIZACIÓN Hasta ahora sé que la excepción ocurre en las siguientes clases de acuerdo con la solicitud realizada, para mí eso no significa nada desafortunadamente.

/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

ACTUALIZACIÓN 2 : las sesiones se almacenan en archivos y el recolector de basura de la sesión de PHP las limpia, ya sea que esta sea una buena opción o no, está fuera del alcance de esta pregunta.

Anton Boritskiy
fuente
Relacionado: maxchadwick.xyz/blog/…
Simon

Respuestas:

24

Después de una depuración avanzada, seguimiento de la sesión y pensar en toda esa magia, pude reproducir el problema y comprender la razón. He preparado una pequeña ilustración de sincronización, puedes verla a continuación.

tiempo problemático

  • la bandera roja es el momento del inicio de sesión del usuario y la creación de sesión
  • La bandera azul es el momento en que el usuario abre la página del catálogo, supongamos que se abre una página de categoría.
  • La bandera verde es el momento en que el usuario envía el pedido ( /sales/order/save/...solicitud)

Aquí está cómo reproducir:

  1. Antes de comenzar: configure el tiempo de espera de la sesión de PHP y el tiempo de espera de las cookies de Magento para que sean 1440, que es un valor predeterminado de PHP.
  2. Elimine todas sus cookies o abra la pestaña de incógnito.
  3. Vaya a su tienda Magento e inicie sesión (ver Bandera 1)
  4. Ir a través del catálogo y agregar algunos productos al carrito (Bandera 2)
  5. Ir a través de la caja y enviar un pedido. Tenga en cuenta el momento en que lo hizo. (Bandera 3)
  6. Ir a través del catálogo y agregar algunos productos al carrito (Bandera 4)
  7. Siga actualizando la página de su carrito o pasando por las páginas del catálogo durante tanto tiempo que el tiempo de espera configurado para las cookies de magento caduque (Banderas 5-6). Tenga en cuenta que el tiempo entre el indicador 7 y el indicador 3 debe ser mayor que el tiempo de espera de la cookie.
  8. Pase por el proceso de pago y envíe un pedido (Marca 7). El envío del pedido fallará debido a la excepción descrita en mi pregunta anterior.

Razón:

Hay ciertas sesiones que solo se instancian en las solicitudes dadas, por ejemplo, Mage_Rss_Model_Sessionsolo se instancia durante el pago real y no mientras se navega por el catálogo. Al mismo tiempo, la marca de tiempo de caducidad de la sesión solo se establece cuando se instancia la sesión. Eso significa que si hubo tiempo suficiente entre dos pagos y la sesión no se canceló mientras tanto (porque el usuario cerró la sesión o la cookie expiró), el nuevo código de Magento considerará que la sesión no pasa la validación y generará una excepción, lo que suena extraño de alguna manera yo.

Como arreglar:

Bueno, tengo pocas opciones:

  1. Espere hasta que Magento reaccione a eso y reconsidere ese código.
  2. Elimina este código mientras tanto.
  3. Intente configurar el tiempo de espera de cookies de Magento en 0 si esa es una opción para usted.

¿Cómo lo descubrí?

  1. Comencé agregando lo siguiente al código original de 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
    );

    me dio una buena idea sobre las clases afectadas y su correlación y la cantidad de sesiones que expiraron. Pero eso no explicaba por qué sucede y qué acciones del usuario conducen al problema.

  2. Luego comencé a pensar en cómo puedo rastrear todos los cambios en los datos de la sesión y encontré esta pregunta /superuser/368231/automatic-versioning-upon-file-change-modify-create-delete que decidí dar un intento gity una incroncombinación, pero después de implementarlo y probarlo en sandbox, me di cuenta de que me quedaría sin espacio en disco muy rápido en producción.

  3. Decidí construir un pequeño script PHP que decodificará los datos de sesión y escribirá registros para cada sesión. Este script fue llamado porincron

    <?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);

    y aquí está la incrontabentrada correspondiente

    /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 $@/$# $%

    salida de muestra

    "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"

PD:

Versiones actuales de ambos

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

no pueden manejar la excepción anterior durante la solicitud de AJAX. ¡Literalmente no muestran nada al usuario, mientras que el usuario se desconecta efectivamente!

PPS:

aparentemente las versiones de Magento CE 1.9.3.x también se ven afectadas, consulte https://github.com/OpenMage/magento-mirror/blame/magento-1.9/app/code/core/Mage/Core/Model/Session/Abstract/ Varien.php

PPPS:

Cuando dije "Eliminar este código mientras tanto". Me refería a excluir el siguiente bloque

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

Puedes hacerlo de muchas maneras, incluyendo:

  1. Simplemente borrando ese bit del archivo
  2. Comentándolo
  3. Volviendo antes
  4. Hacer $this->useValidateSessionExpire()realidad el retorno
  5. ...
  6. Es programación, sea creativo;)
Anton Boritskiy
fuente
Acabo de deshabilitar <Mage_Rss>y eso solucionó el problema (solución temporal) y he presentado el ticket con soporte de magento.
Damodar Bashyal
1
@DamodarBashyal, tenga en cuenta que el problema no solo afecta el pago. También afecta las páginas de productos, creo que algunas otras páginas también podrían verse afectadas. Motivo: se inicializa un conjunto diferente de objetos de sesión en cada acción del controlador magento. Puedo proporcionar más explicaciones si es necesario.
Anton Boritskiy
Tuve un problema con la API, al crear el envío recibí un error. Leer estuvo bien, pero el problema fue con la escritura hasta que se deshabilitó. Gracias por la información.
Damodar Bashyal
9

6. Es programación - sea creativo;)

Otra forma de arreglar esto (y mejorar la validación de la sesión)

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

El código de sesión actualmente almacena los datos del validador de sesión dentro de cada espacio de nombres y también los valida cada vez que se inicia el espacio de nombres. Esto es malo porque:

  1. Extremadamente ineficiente de espacio de almacenamiento de sesión. Los datos del validador a menudo comprenden más del 50% del espacio utilizado por un espacio de nombres y cuando hay muchos espacios de nombres, esto se suma a una tonelada de desperdicio. El almacenamiento de la sesión se puede cortar drásticamente con este parche y cuando se usa un almacenamiento en memoria como Redis o Memcached, eso es muy importante.
  2. La ineficiencia de los ciclos de cálculo ya que múltiples espacios de nombres significa múltiples validaciones y no hay una buena razón para que estos difieran entre sí.
  3. En realidad, crea errores como # 394 donde los datos del validador se actualizan en algunas solicitudes pero no en otras (por lo que puede diferir pero no debería). No lo he probado pero creo que esto también solucionará este problema.
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()

Fuente: https://github.com/OpenMage/magento-lts/commit/de06e671c09b375605a956e100911396822e276a


Actualizar:

Solución para la web/session/use_http_x_forwarded_for optionopción deshabilitada ... https://github.com/OpenMage/magento-lts/pull/457/commits/ec8128b4605e82406679c3cd81244ddf3878c379

sv3n
fuente
1
eso se ve bien en realidad, ¿alguna experiencia usando eso en producción?
Anton Boritskiy
@AntonBoritskiy Sí, uso esto en producción. Funciona perfecto
sv3n
sv3n ¿hay algún lado malo potencial de este método de solución?
Vaishal Patel
@VaishalPatel si hay posibles lados malos, no los veo en realidad :) Utilizo esto en producción y resolvió todos los problemas de validación de sesión. No publicaría esto si tuviera alguna inquietud, pero si tiene dudas, pregunte aquí: github.com/OpenMage/magento-lts/pull/406 . ¿Quizás algunos de los "profesionales" de SE también tienen tiempo para revisar esto?
sv3n
Pondré en mi producción. De cualquier manera, está progresando hacia una solución.
Vaishal Patel
1

¿Cómo estás almacenando las sesiones? (es decir, en var / session / o en la base de datos, o utilizando otros motores de almacenamiento en caché como Redis o Memcached)

Independientemente de lo que esté usando, asegúrese de que sus permisos de escritura sean correctos var/session/(generalmente configurados en 755 para directorios y 644 para archivos), o si está usando Redis o Memcache, asegúrese de que su conexión y la configuración de tiempo de espera sean buenos para esos .

Inchoo tiene un buen tutorial para Redis: http://inchoo.net/magento/using-redis-cache-backend-and-session-storage-in-magento/

Si usa Memcache, consulte este artículo (hace referencia a v1.10, pero no debería ser muy diferente): http://www.magestore.com/magento/magento-sessions-disappearing-with-memcache-turned-on.html

Además, si está usando algo como el barniz, ha habido problemas en el pasado con sesiones en las que se necesitaban perforar ciertas páginas.

Finalmente, si está utilizando el sistema de archivos para sus sesiones, puede encontrar alivio simplemente cambiando el <session_save>nodo en local.xml"db" en lugar de "archivos".

De esto <session_save><![CDATA[files]]></session_save>

A esto <session_save><![CDATA[db]]></session_save>

gtr1971
fuente
gracias por la sugerencia: debería haber agregado la información a la pregunta sobre cómo almaceno las sesiones, las guardo en archivos. Acabo de descubrir el problema original, considero que es un error de Magento. Lo terminaré y publicaré una respuesta en breve
Anton Boritskiy
¡Genial! ... ¿Mi respuesta ayudó en absoluto con la solución?
gtr1971
en realidad no - vea mi respuesta
Anton Boritskiy
0

El detalle de Anton Boritskiy es fantástico. Pero en lugar de excluir este bloque, puede hacer una copia local para no editar el núcleo y reescribir el bloque como:

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

Esto asegura que la comparación entre time () y session_expire_timestamp solo se ejecuta cuando existe la clave y que cuando se encuentra una sesión que no tiene la clave (es decir, una sesión anterior a 1.9.3), se agrega la clave.

Vaishal Patel
fuente
Por supuesto, agregar una copia local y anular es mucho mejor que modificar los archivos principales, mantenemos internamente la lista de parches que se aplican automáticamente durante la compilación del proyecto, porque Magento estaba lanzando un par de errores como ese recientemente.
Anton Boritskiy
Al mismo tiempo, no veo cómo su cambio soluciona el problema original, ¿podría agregar una explicación un poco más amplia?
Anton Boritskiy
Anto Boritskiy que es un buen grito con la lista.
Vaishal Patel
Anto Boritskiy, La nueva clave se usa para verificar la validez de la marca de tiempo de la sesión. $ sessionData proviene de $ this -> _ data [self :: VALIDATOR_KEY]; pero la clave session_expire_timestamp solo se agrega a la sesión mediante $ this-> getValidatorData (); funciona y se almacena en $ this -> _ datos [...] al final de la llamada a la función. Por lo tanto, el problema es que en las sesiones existentes esta clave session_expire_timestamp no está disponible.
Vaishal Patel