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

Magento使用最新发行版将此if块添加到文件中。这显然是一种制动变化,请参阅下面的更多详细信息。

例外情况经常发生,例如每天十几次。但是除非我在上面的条件中完全符合条件,否则我无法重新创建导致异常的条件。例外最常见于产品详细信息页面和一页结帐的最后一步。该商店是b2b商店,用户必须登录才能查看产品页面或能够结帐,这意味着会话无效/过期时,用户将被重定向到登录页面。目前,对我来说更重要的是在结帐时解决此问题。

从用户的角度来看,发生了什么:用户填充购物车,继续进行结帐并到达最后一步,然后他/她单击“提交订单”按钮,但没有任何反应。在后台,Magento的JS执行AJAX请求,并且JS希望收到JSON,但是如果发生此错误,则会返回登录页面的HTML,JavaScript无法解析该HTML,并且它什么也不做。这对于用户而言超级混乱。

好吧,这不是完整的用户方案,我们联系了用户,他们告诉我们,他们在填充购物车和提交订单之间等待了几天,这实际上很难弄清楚,因为人们根本不记得这一点。

PHP会话生存期-350000(以秒为单位约4天)Cookie生存期-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会话垃圾收集器清理,无论这是一个不错的选择,这都不在本问题的范围之内。


Answers:


24

经过一些高级调试,会话跟踪以及对所有魔术的思考,我得以重现该问题并理解其原因。我准备了一些时序图,您可以在下面看到它。

问题时间

  • 危险标志是用户登录和会话创建的时刻
  • 蓝旗是用户打开目录页面的时刻,我们假设它是打开的类别页面。
  • 绿色标记是用户提交订单(/sales/order/save/...请求)的时刻

复制方法如下:

  1. 开始之前:将PHP会话超时和Magento cookie超时都设置为1440,这是默认的PHP值。
  2. 杀死所有Cookie或打开隐身标签。
  3. 前往您的Magento商店并登录(请参见标志1)
  4. 浏览目录并将一些产品添加到购物车(标志2)
  5. 通过结帐并提交订单。请注意您完成操作的时间。(标志3)
  6. 浏览目录并将一些产品添加到购物车(标志4)
  7. 持续刷新购物车页面或浏览目录页面,直到为magento cookie配置的超时时间到期(标志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尝试gitincron组合,但是在实现它并在沙箱中对其进行测试后,我意识到在生产中我将很快用完磁盘空间。

  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支持。
Damodar Bashyal

1
@DamodarBashyal请注意,该问题不仅影响结帐。它也会影响产品页面,我相信其他一些页面也可能会受到影响。原因-每个magento控制器动作都会初始化不同的会话对象集。如果需要,我可以提供更多解释。
安东·博里茨基

API出现问题,在创建货件时出现错误。读还可以,但是直到禁用它为止,写都是问题。谢谢。
Damodar Bashyal

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这种解决方法是否有潜在的弊端?
Vaishal Patel

@VaishalPatel,如果有任何潜在的不利方面,我实际上看不到它们:)我在生产中使用了它,它解决了所有会话验证问题。如果我有任何疑问,我不会发布,但是如果您有疑问,请在这里询问:github.com/OpenMage/magento-lts/pull/406。也许某些SE“专家”也有一些时间对此进行审查?
sv3n

我将投入生产。无论哪种方式,它都在寻求解决方案。
Vaishal Patel

1

您如何存储会话?(即在var / session /中或在数据库中,或使用其他缓存引擎,例如Redis或Memcached)

无论您使用的是哪种var/session/格式,请确保您的写入权限正确(对于dirs而言,通常设置为755,对于文件而言,通常设置为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错误。我会总结一下并在不久后发布答案
Anton Boritskiy

太好了!...解决方案对我的回答没有帮助吗?
gtr1971 '17

并非如此-请看我的回答
Anton Boritskiy

0

Anton Boritskiy的细节很棒。但是您可以创建本地副本,而不是排除此块,因此您无需编辑核心并像下面这样重写该块:

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最近发布了一些错误。
安东·博里茨基

同时,我看不到您的更改如何解决原始问题,是否可以添加更多扩展说明?
安东·博里茨基

Anto Boritskiy很好地支持了这份名单。
Vaishal Patel

Anto Boritskiy,新密钥用于检查会话时间戳的有效性。$ sessionData来自$ this-> _ data [self :: VALIDATOR_KEY]; 但是session_expire_timestamp键仅由$ this-> getValidatorData()添加到会话中;函数并在函数调用结束时存储在$ this-> _ data [...]中。因此,问题在于在现有会话中,此session_expire_timestamp密钥不可用。
Vaishal Patel
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.