Falha na validação da sessão no Magento 1 EE v 1.14.3.x (e CE 1.9.3.x)

18

Estou cuidando de uma loja Magento com 400 a 500 visitantes e 40 a 50 pedidos por dia. Recentemente, o sistema foi atualizado do Magento EE 1.14.2.4 para o Magento EE 1.14.3.2 e notei algumas exceções estranhas nos logs:

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

Eu estava perseguindo essa exceção e sei que ela está sendo acionada porque o seguinte código de validação da sessão falha ao validar a sessão:

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 bloco if foi adicionado ao arquivo com a versão mais recente do Magento. E esta é uma mudança de frenagem, aparentemente, veja mais detalhes abaixo.

A exceção está acontecendo com bastante frequência, como uma dúzia de vezes por dia. mas não consigo recriar condições que levam à exceção, a menos que eu literalmente aplique a condição acima. As exceções costumam ocorrer nas páginas de detalhes do produto e na última etapa do checkout de uma página. A loja é uma loja b2b, o usuário deve estar logado para ver a página do produto ou para fazer o checkout, significa que o usuário é redirecionado para as páginas de login quando a sessão é invalidada / expirada. No momento, é mais importante para mim corrigir esse problema durante o checkout.

O que acontece da perspectiva do usuário: O usuário preenche o carrinho, faz o checkout e alcança o último passo, depois pressiona o botão "enviar o pedido" e nada acontece. Nos bastidores, o JS do Magento realiza uma solicitação AJAX e o JS espera receber o JSON de volta, mas se esse erro ocorrer, o HTML da página de login será retornado, o que não pode ser analisado pelo JavaScript e simplesmente não faz nada. Isso é super confuso para os usuários.

Bem, esse não é o cenário completo do usuário, contatamos os usuários e eles nos disseram que esperavam alguns dias entre encher o carrinho e enviar o pedido, o que isso significa exatamente é difícil de entender, porque as pessoas simplesmente não se lembram disso.

Duração da sessão do PHP - 350000 (~ 4 dias em segundos) Tempo de vida do cookie - 345600 (4 dias)

Aqui está a pergunta real: como descobrir que tipo de comportamento do usuário leva à exceção?

ATUALIZAÇÃO Até agora eu sei que a exceção acontece nas aulas seguintes, de acordo com a solicitação feita, para mim isso infelizmente não significa nada.

/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

ATUALIZAÇÃO 2 : as sessões são armazenadas em arquivos e limpas pelo coletor de lixo da sessão do PHP, se essa é uma boa opção ou não, está fora do escopo desta pergunta.

Anton Boritskiy
fonte
Relacionados: maxchadwick.xyz/blog/...
Simon

Respostas:

24

Depois de alguma depuração avançada, rastreamento de sessão e reflexão sobre toda essa mágica, consegui reproduzir o problema e entender o motivo. Eu preparei uma pequena ilustração de tempo, você pode ver abaixo.

hora do problema

  • bandeira vermelha é o momento do login do usuário e da criação da sessão
  • bandeira azul é o momento em que o usuário abre a página de catálogo, vamos assumir que é uma página de categoria que é aberta.
  • bandeira verde é o momento em que o usuário envia o pedido ( /sales/order/save/...solicitação)

Aqui está como reproduzir:

  1. Antes de começar: Defina o tempo limite da sessão do PHP e o tempo limite do cookie Magento como 1440, que é um valor padrão do PHP.
  2. Mate todos os seus cookies ou abra a guia anônima.
  3. Vá para a sua loja Magento e faça o login (consulte a Bandeira 1)
  4. Acesse o catálogo e adicione alguns produtos ao carrinho (Bandeira 2)
  5. Faça o checkout e envie um pedido. Observe a hora em que você fez isso. (Bandeira 3)
  6. Navegue pelo catálogo e adicione alguns produtos ao carrinho (Bandeira 4)
  7. Continue atualizando a página do carrinho ou passando por páginas do catálogo por tanto tempo que o tempo limite que você configurou para os cookies magento expira (Bandeiras 5-6). Observe que o tempo entre o Sinalizador 7 e o Sinalizador 3 deve ser maior que o tempo limite do cookie.
  8. Faça o checkout e envie um pedido (Bandeira 7). O envio do pedido falhará devido à exceção descrita na minha pergunta acima.

Razão:

Há certas sessões que são instanciadas apenas em determinadas solicitações, por exemplo, Mage_Rss_Model_Sessionsão instanciadas apenas durante o checkout real e não durante a navegação no catálogo. Ao mesmo tempo, o registro de data e hora de expiração da sessão é definido apenas quando a sessão foi instanciada. Isso significa que, se houver tempo suficiente entre dois checkouts e a sessão não for interrompida (porque o usuário desconectou ou o cookie expirou), o novo código Magento considerará essa sessão como não passando na validação e lançará uma exceção, o que parece estranho para mim.

Como consertar:

Bem, eu tenho algumas opções:

  1. Espere até o Magento reagir a isso e reconsiderar esse código.
  2. Remova esse código enquanto isso.
  3. Tente definir o tempo limite do cookie Magento como 0 se essa for uma opção para você.

Como eu descobri isso:

  1. Comecei adicionando o seguinte ao 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
    );

    isso me deu uma boa visão sobre as classes afetadas e sua correlação e quanto da sessão expirou. Mas isso não explicava por que isso acontece e quais ações do usuário levam ao problema.

  2. Então comecei a pensar em como rastrear todas as alterações nos dados da sessão e me deparei com esta pergunta /superuser/368231/automatic-versioning-upon-file-change-modify-create-delete que decidi dar uma tentativa gite incroncombinação, mas depois de implementá-lo e testá-lo na sandbox, percebi que o espaço em disco ficaria muito rápido na produção.

  3. Decidi construir um pequeno script PHP que decodificaria os dados da sessão e gravaria logs para cada sessão. Este script foi chamado 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);

    e aqui está a incrontabentrada correspondente

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

    saída de amostra

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

Versões atuais de ambos

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

não conseguem lidar com a exceção acima durante a solicitação AJAX. Eles exibem literalmente nada para o usuário, enquanto o usuário efetivamente é desconectado!

PPS:

aparentemente as versões do Magento CE 1.9.3.x também são afetadas, consulte https://github.com/OpenMage/magento-mirror/blame/magento-1.9/app/code/core/Mage/Core/Model/Session/Abstract/ Varien.php

PPPS:

Quando eu disse "Remova esse código enquanto isso". Eu quis dizer excluir o seguinte bloco

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

você pode fazer isso de várias maneiras, incluindo:

  1. Simplesmente excluindo esse bit do arquivo
  2. Comentando
  3. Voltando antes
  4. Tornando o $this->useValidateSessionExpire()retorno verdadeiro
  5. ...
  6. É programação - seja criativo;)
Anton Boritskiy
fonte
Acabei de desativar <Mage_Rss>e isso corrigiu o problema (correção temporária) e apresentou o tíquete com suporte ao magento.
Damodar Bashyal
11
@DamodarBashyal, lembre-se de que o problema não afeta apenas a finalização da compra. Também afeta as páginas do produto. Acredito que algumas outras páginas também possam ser afetadas. Razão - um conjunto diferente de objetos de sessão é inicializado em cada ação do controlador magento. Eu posso fornecer mais explicações, se necessário.
Anton Boritskiy
Eu tive um problema com a API, ao criar a remessa estava recebendo erro. A leitura estava correta, mas o problema ocorreu com a gravação até ser desativada. Thx para informações.
Damodar Bashyal
9

6. É programação - seja criativo;)

Outra maneira de corrigir isso (e melhorar a validação da sessão)

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

O código da sessão atualmente armazena os dados do validador da sessão em todos os espaços para nome e também os valida toda vez que o espaço para nome é iniciado. Isso é ruim porque:

  1. Extremamente ineficiente de espaço de armazenamento da sessão. Os dados do validador geralmente compreendem mais de 50% do espaço usado por um espaço para nome e, quando existem muitos espaços para nome, isso gera uma tonelada de desperdício. O armazenamento da sessão pode ser cortado drasticamente com esse patch e ao usar um armazenamento na memória como Redis ou Memcached, que importa muito.
  2. Ineficiente de ciclos de computação, pois vários espaços para nome significam várias validações e não há uma boa razão para que eles diferem entre si.
  3. Na verdade, cria bugs como o # 394, onde os dados do validador são atualizados em algumas solicitações, mas não em outras (para que possam diferir, mas não devem). Não testei, mas acredito que isso também resolverá esse 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()

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


Atualizar:

Correção para a web/session/use_http_x_forwarded_for optionopção desativada ... https://github.com/OpenMage/magento-lts/pull/457/commits/ec8128b4605e82406679c3cd81244ddf3878c379

sv3n
fonte
11
parece bom, na verdade, alguma experiência usando isso na produção?
Anton Boritskiy
@AntonBoritskiy Sim, eu uso isso na produção. Funciona perfeito.
Sv3n
sv3n existem possíveis lados negativos nesse método de solução?
Vaishal Patel
@VaishalPatel se houver algum lado ruim em potencial, eu não o vejo realmente :) Eu uso isso na produção e resolveu todos os problemas de validação de sessão. Eu não publicaria isso se tivesse alguma dúvida, mas se você tiver dúvidas, pergunte aqui: github.com/OpenMage/magento-lts/pull/406 . Talvez alguns dos "profissionais" do SE também tenham algum tempo para revisar isso?
Sv3n
Vou colocar na minha produção. De qualquer maneira, está progredindo em direção a uma solução.
Vaishal Patel
1

Como você está armazenando sessões? (ou seja, em var / session / ou no DB, ou usando outros mecanismos de armazenamento em cache, como Redis ou Memcached)

Qualquer que seja o seu uso, verifique se as permissões de gravação estão corretas var/session/(normalmente definidas como 755 para diretórios e 644 para arquivos) ou, se você estiver usando Redis ou Memcache, verifique se as configurações de conexão e tempo limite são boas para aqueles .

O Inchoo tem um bom tutorial para Redis: http://inchoo.net/magento/using-redis-cache-backend-and-session-storage-in-magento/

Se estiver usando o Memcache, consulte este artigo (ele faz referência à v1.10, mas não deve ser muito diferente): http://www.magestore.com/magento/magento-sessions-disappearing-with-memcache-turned-on.html

Além disso, se você estiver usando algo como o Varnish, houve problemas no passado com sessões em que certas páginas eram necessárias.

Finalmente, se você estiver usando o sistema de arquivos para suas sessões, poderá encontrar alívio simplesmente alternando o <session_save>nó no seu local.xmlpara "db" em vez de "arquivos".

A partir disso <session_save><![CDATA[files]]></session_save>

Para isso <session_save><![CDATA[db]]></session_save>

gtr1971
fonte
obrigado pela dica - eu deveria ter adicionado as informações à pergunta sobre como armazenar sessões, na verdade, as armazeno em arquivos. Acabei de descobrir a questão original, considero isso um bug do Magento. Eu vou envolvê-lo e postar uma resposta em breve
Anton Boritskiy
Ótimo! ... Minha resposta ajudou em tudo com a solução?
gtr1971
não é realmente - ver a minha resposta
Anton Boritskiy
0

Os detalhes de Anton Boritskiy são fantásticos. Mas, em vez de excluir esse bloco, você pode fazer uma cópia local para não editar o núcleo e reescrever o bloco 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];
}

Isso garante que a comparação entre time () e o session_expire_timestamp seja executada apenas quando a chave existir e que, quando for encontrada uma sessão que não possua a chave (ou seja, uma sessão anterior à 1.9.3), a chave será adicionada.

Vaishal Patel
fonte
Adicionar uma cópia local e substituir é obviamente muito melhor do que modificar os arquivos principais; mantemos internamente uma lista de patches que são automaticamente aplicados durante a construção do projeto, porque o Magento estava lançando alguns bugs recentemente.
Anton Boritskiy
Ao mesmo tempo, não vejo como sua alteração corrige o problema original. Poderia adicionar uma explicação um pouco mais expandida?
Anton Boritskiy
Anto Boritskiy, que é um bom grito com a lista.
Vaishal Patel
Anto Boritskiy, A nova chave é usada para verificar a validade do registro de data e hora da sessão. $ sessionData vem de $ this -> _ data [self :: VALIDATOR_KEY]; mas a chave session_expire_timestamp é adicionada apenas à sessão pelo $ this-> getValidatorData (); função e armazenada em $ this -> _ data [...] no final da chamada de função. Portanto, o problema é que, nas sessões existentes, essa chave session_expire_timestamp não está disponível.
Vaishal Patel