Yii1 / yii2身份验证共享

图片

如果没有第一部分 ,本文就没有意义,在第一部分中有一个答案“为什么要这样做”。

它是关于将项目从yii1顺利迁移到yii2的技术。 其实质是yii1上的项目分支和yii2上的新版本在一个虚拟主机中的同一域上一起工作,并且迁移是逐步进行的,而且步调很小(通过页面,控制器,模块等)。

第一部分是关于如何在现有虚拟主机上的yii2上运行一个裸项目。 使两个分支机构一起工作而不相互干扰。

之后,最困难的阶段开始了:您需要为开始创建一个最小的基础架构。 我将挑出2个任务:重复设计和端到端用户身份验证。

设计的重复是无聊的第一位。 如果您不走运,只需复制/还原旧的“ 1合1”即可。 就我个人而言,我总是与重新设计结合在一起。 即 界面和设计已得到重大更新,在这方面,工作并不愚蠢。 但这里有他自己的一面-我非常注意界面和设计,恰恰相反,有人更喜欢后端和控制台。 但是,无论喜好如何,都无法克服此任务-您将必须创建一个界面,并且工作量将很大。

端到端身份验证更加有趣,并且工作量将减少。 与第一篇文章一样,不会有任何启示。 本文的特征:首次解决此问题的人员的教程。

如果这是您的情况,那么请参考更多详细信息

首先,您需要在分支之间拆分功能。 因为 可以理解,迁移阶段只是在开始阶段,然后很可能所有与用户进行的工作(注册,身份验证,密码恢复等)都保留在旧站点上。 yii2上的新分支应该只看到已经通过身份验证的用户。

yii1 / yii2的身份验证机制略有不同,您需要调整yii2代码,以便它可以看到经过身份验证的用户。 因为 身份验证数据存储在会话中,您只需要在用于读取会话数据的参数上达成一致即可。

在来自yii1的会话中,这是以某种方式存储的:

print_r($_SESSION); Array ( [34e60d27092d90364d1807021107e5a3__id] => 123456 [34e60d27092d90364d1807021107e5a3__name] => tester [34e60d27092d90364d1807021107e5a3__states] => Array ( ) ) 

它如何与您一起存储-例如,检查yii1的不同版本中prefixKey的生成方式是否不同。

这是您需要的yii1数据

 Yii::app()->user->getStateKeyPrefix() Yii::app()->name Yii::app()->getBasePath() Yii::app()->getId() get_class(Yii::app()->user) 

最简单的方法是制作测试页并在其上显示所有必要的数据-将来将需要它们。

yii2中的身份验证


在Yii2中,我们需要的所有功能都位于用户组件( \ yii \ web \ User )中,该组件控制身份验证状态。

  • 在其getIdentity()方法中, 调用了renewAuthStatus() ,其中,通过$ idParam变量中的密钥查找身份验证会话(默认情况下,其中存储了“ __id”);
  • 在会话变量中,用户ID是使用键$ idParam存储的(例如,来自app / model / User )。

认证算法在官方指南中有详细描述

当然,在yii1中,会话使用其他密钥存储。 因此,您需要使yii2使用与yii1中存储的相同的键来查找用户ID。

为此:

1.将负责管理身份验证状态的用户组件的类更改为我们自己的类,该类继承自config / web.php中的yii / web / User

 'components' => [ 'user' => [ 'class' => 'app\models\WebUser', 'identityClass' => 'app\models\User', ], ] 

2.在app \ models \ WebUser中调整$ idParam的值。

 public function init() { //  idParam (    //    ) $this->idParam = $this->getIdParamYii1(); } 

在扰流器下,将有一些方法以某种方式模仿yii1的类似行为。

通常,您可以仅从yii1复制原始的_keyPrefix (甚至立即复制idParam ),而不模拟其生成,但是就像“复制难以理解的垃圾”指令一样。

确实,您可以复制,因为yii1中的_keyPrefix几乎是静态的。 它取决于用户组件的类名称和应用程序ID,而应用程序ID又从应用程序的位置及其名称获得。

 //   yii1  _keyPrefix $this->_keyPrefix = md5('Yii.'.get_class($this).'.'.Yii::app()->getId()); //   - ID  $this->_id=sprintf('%x',crc32($this->getBasePath().$this->name)); 

如果我们仅将自己限制在身份验证任务中,则复制_keyPrefix值将大大减少工作量。 但我将举例说明更广泛的用途。

用户组件(app \ models \ WebUser)
 namespace app\models; use yii\web\User; class WebUser extends User { /** *    cookies  yii2, *     yii1 */ public $autoRenewCookie = false; /** * _keyPrefix    CWebUser  Yii1 */ private $_keyPrefix; /** *    Yii1,    */ private $paramsYii1 = [ //    user   Yii1 'classUserComponent' => 'CWebUser', // ID  Yii1 //  ,  Yii::app()->getId() 'appId' => '', //     Yii1 'appName' => 'My Web Application', //     yii1  \Yii::getAlias('@app') 'relPath' => '../htdocs/protected', ]; public function init() { //  idParam (    //    ) $this->idParam = $this->getIdParamYii1(); } } 

以及其他方法(WebUser)。 分开以便于观看。

 /** *     Yii1,    ID  */ public function getIdParamYii1() { return $this->getStateKeyPrefix() . '__id'; } /** *    Yii 1 * @return string */ public function getStateKeyPrefix() { if ($this->_keyPrefix !== null) return $this->_keyPrefix; $class = $this->paramsYii1['classUserComponent']; return $this->_keyPrefix = md5('Yii.' . $class . '.' . $this->getAppIdYii1()); } /** *   getId()  CApplication * @return string ID  Yii1 */ public function getAppIdYii1() { if ($this->paramsYii1['appId']) return $this->paramsYii1['appId']; return $this->paramsYii1['appId'] = sprintf('%x', crc32($this->getBasePathYii1() . $this->paramsYii1['appName'])); } /** * @return string    Yii1 */ private function getBasePathYii1() { $basePath = realpath(\Yii::getAlias('@app') . DIRECTORY_SEPARATOR . $this->paramsYii1['relPath']); if (!$basePath) throw new InvalidConfigException('basePath  yii1  .'); return $basePath; } 

仅对于“ 同意会话密钥的格式 ”的任务,方法的分解有点复杂,但是它们对于以下示例很有用。

之后,在yii2上的新分支中,对先前在yii1中授权的用户的识别开始起作用。 理想情况下,必须停止在此位置,因为滑行路径会进一步开始。

yii2中的用户登录


同意会话中用户ID的存储格式后,即使“自动”用户登录也可能会通过yii2起作用。

我认为在战斗模式下使用yii2上的登录表单而不禁用yii1上的相应表单是不正确的,但是,基本的功能在稍加协调之后就可以使用。

因为 我们认为,在进行用户注册以及相应的密码哈希处理后,我们留在yii1中,然后对于通过yii2进行的有效登录,我们需要确保yii2中保存的密码的验证方法可以理解在Yii1中进行了哈希处理和存储的内容。

检查这些方法的作用。

例如,如果在Yii2中,条件标准用户模型User将按以下方式验证密码:

 public function validatePassword($password) { return \Yii::$app->getSecurity()->validatePassword($password, $this->password); } 

然后,从yii \ base \ Security (Yii2)中查看validatePassword($ password,$ hash) 方法

validatePassword()
 public function validatePassword($password, $hash) { if (!is_string($password) || $password === '') { throw new InvalidArgumentException('Password must be a string and cannot be empty.'); } if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30 ) { throw new InvalidArgumentException('Hash is invalid.'); } if (function_exists('password_verify')) { return password_verify($password, $hash); } $test = crypt($password, $hash); $n = strlen($test); if ($n !== 60) { return false; } return $this->compareString($test, $hash); } 

如果在Yii1上,用户模型中的密码哈希是这样完成的:

 public function hashPassword($password) { return CPasswordHelper::hashPassword($password); } 

然后与yii \ framework \ utils \ CPasswordHelper中的 verifyPassword($ password,$ hash)进行比较

hashPassword()
 public static function hashPassword($password,$cost=13) { self::checkBlowfish(); $salt=self::generateSalt($cost); $hash=crypt($password,$salt); if(!is_string($hash) || (function_exists('mb_strlen') ? mb_strlen($hash, '8bit') : strlen($hash))<32) throw new CException(Yii::t('yii','Internal error while generating hash.')); return $hash; } 


如果哈希和验证方法不同,则需要在app \ model \ User中的validatePassword()中更改验证。

从框架的最新版本的框中,可以兼容Yii1 / Yii2密码哈希。 但这并不意味着它们将与您兼容,或者将来会重合。 Yii1中项目的哈希方法和Yii2新项目中的验证方法的概率很高。

在Yii2中自动登录yii1的cookie


由于Yii2上的分支机构已经知道如何透明地使用Yii1的用户身份验证数据,为什么不通过cookie设置自动登录?
如果您有这种想法,建议您不要这样做。 我认为没有充分的理由在不将用户工作(首先是身份验证)转移到此线程的情况下在Yii2上启用自动登录。 即 我的意思是以下情况:

用户身份验证是在Yii1上执行的,但是Yii2分支应该能够对Yii1中存储的cookie进行自动登录。

老实说,这是一个坦率的变态。 这样做并非易事而优雅,只是在这之间的界限介于有意识和合理的移徙任务与不必要的自行车的发明之间。

困难在于,Yii的两个分支机构都受到保护,免受假Cookie的影响,因此很难就方法达成一致。

  • Yii2涉及的组件是:用户( \ yii \ web \ User ), 请求安全性 + CookieCollection
  • 在Yii1中: CWebUserCHttpRequestCSecurityManagerCStatePersisterCCookieCollection

但是,情况不同。 以下是如何使用自行车进行自动登录的示例。

在yii2中,我们对\ yii \ web \ User中的getIdentityAndDurationFromCookie()方法感兴趣。 在此方法的第一行中,应获取所需的cookie:

 $value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']); 

但是她不会,因为 Yii :: $ app-> getRequest()-> getCookies()集合将为空,因为在Request cookie中,验证是通过loadCookies()加载的,并且当然不会通过。

分叉最简单的方法是通过覆盖getIdentityAndDurationFromCookie()来实现标准行为。 例如,像这样:

  1. 直接从超全局$ _COOKIE下载所需的cookie,以免破坏标准的cookie加载机制。

    标识cookie的名称仅为_keyPrefix,我们已经知道如何接收(或复制)了。 因此,我们在init()中更改了标准$ identityCookie
  2. “大约与yii1中一样”解密所得的cookie。 如你所愿。 例如,我从CSecurityManager复制了必需的方法。

实际上,下面是代码。

我们在应用程序/模型/ WebUser中工作
1.按照ii1设置的名称identityCookie cookie

 public function init() { $this->idParam = $this->getIdParamYii1(); //     $this->identityCookie = ['name' => $this->getStateKeyPrefix()]; } 


2.再添加两种方法

 /** *    */ protected function getIdentityAndDurationFromCookie() { $id = $this->getIdIdentityFromCookiesYii1(); if (!$id) { return null; } $class = $this->identityClass; $identity = $class::findOne($id); if ($identity !== null) { return ['identity' => $identity, 'duration' => 0]; } return null; } /** *  ID identity  cookies,   yii1 * * @return null|integer  ID     null */ protected function getIdIdentityFromCookiesYii1() { if (!isset($_COOKIE[$this->identityCookie['name']])) return null; $cookieValue = $_COOKIE[$this->identityCookie['name']]; // Cookies  yii1 ,  $utilSecurity = new UtilYii1Security($this->getBasePathYii1()); $data = $utilSecurity->validateData($cookieValue); if ($data === false) { return null; } $data = @unserialize($data); if (is_array($data) && isset($data[0], $data[1], $data[2], $data[3])) { list($id, $name, $duration, $states) = $data; return $id; } return null; } 


该代码使用某个UtilYii1Security类-这是对CSecurityManager的必要方法的修改后的复制粘贴,因此,一方面看起来像原始的,但有一些简化。 例如,在CSecurityManager中,有多个用于生成HMAC(基于哈希的消息身份验证代码)的选项,这取决于php版本和mbstring的存在。 但是因为 已知yii1与yii2在相同的环境中工作,然后简化了任务,并因此简化了代码。

因为 很显然,这里写了一个明显的拐杖,所以没有必要尝试使其通用并赋予它优美的形状,足以在您的条件下对其进行“研磨”。

UtilYii1Security.php
 <?php namespace app\components; /* *   CSecurityManager  Yii1, *     * */ use yii\base\Exception; use yii\base\Model; use yii\base\InvalidConfigException; class UtilYii1Security { /** * ,   yii1 */ const STATE_VALIDATION_KEY = 'Yii.CSecurityManager.validationkey'; /** *  ,    yii1 */ public $hashAlgorithm = 'sha1'; /** *   cookies */ private $_validationKey; /** *    Yii1 */ private $basePath; /** *      yii1 */ private $stateFile; /** * @param string $basePath -    Yii1 *        stateFile */ public function __construct($basePath) { $this->basePath = $basePath; $this->stateFile = $this->basePath . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . 'state.bin'; if (!realpath($this->stateFile)) throw new InvalidConfigException('    '); } public function validateData($data, $key = null) { if (!is_string($data)) return false; $len = $this->strlen($this->computeHMAC('test')); if ($this->strlen($data) >= $len) { $hmac = $this->substr($data, 0, $len); $data2 = $this->substr($data, $len, $this->strlen($data)); return $this->compareString($hmac, $this->computeHMAC($data2, $key)) ? $data2 : false; } else return false; } public function computeHMAC($data, $key = null) { if ($key === null) $key = $this->getValidationKey(); return hash_hmac($this->hashAlgorithm, $data, $key); } public function getValidationKey() { if ($this->_validationKey !== null) return $this->_validationKey; if (($key = $this->loadStateValidationKey(self::STATE_VALIDATION_KEY)) !== null) { $this->_validationKey = $key; } return $this->_validationKey; } //  validationKey    Yii1 private function loadStateValidationKey($key) { $content = $this->loadState(); if ($content) { $content = unserialize($content); if (isset($content[$key])) return $content[$key]; } return false; } //      Yii1 protected function loadState() { $filename = $this->stateFile; $file = fopen($filename, "r"); if ($file && flock($file, LOCK_SH)) { $contents = @file_get_contents($filename); flock($file, LOCK_UN); fclose($file); return $contents; } return false; } public function compareString($expected, $actual) { $expected .= "\0"; $actual .= "\0"; $expectedLength = $this->strlen($expected); $actualLength = $this->strlen($actual); $diff = $expectedLength - $actualLength; for ($i = 0; $i < $actualLength; $i++) $diff |= (ord($actual[$i]) ^ ord($expected[$i % $expectedLength])); return $diff === 0; } private function strlen($string) { return mb_strlen($string, '8bit'); } private function substr($string, $start, $length) { return mb_substr($string, $start, $length, '8bit'); } } 


行动顺序


从身份验证的方式从yii1迁移到yii2时,我遵循以下顺序:

  1. 在分支之间进行透明的用户身份验证。
    即 以便yii2分支接受通过yii1认证的用户。 它很快,并不困难。
  2. 将身份验证(用户登录)从yii1转移到yii2。
    在旧分支中同时禁用它。 请注意,此后,cookie的自动登录将停止工作,因为 yii1中的cookie不再适合,并且yii2上仍然很少有新页面。
  3. 至少将网站的主页移植到yii2
    这样您就可以对存储在yii2中的新cookie使用自动登录。
    自动登录的存在(至少在主登录中)将有助于掩盖前一个分支上丢失的自动登录。
  4. 检查yii1是否了解yii2中已认证的身份。
    通过协商会话密钥。
  5. 将用户注册转移到yii2。
    传输必须在先前存储的密码哈希的协调下完成。 也许保留旧的哈希格式或引入新的哈希格式,但这样登录名就可以理解这两种类型。
  6. 考虑为用户添加站点服务,以使yii2“开箱即用”。
    我的意思是实现用户的IdentityInterface接口,该接口允许通过令牌进行身份验证,恢复密码等。 也许您已经拥有合适的安全带,但是突然之间没有? 然后,这是一个以最小的努力改进服务的好选择。

    如果是,那么这将导致在yii2中(至少部分地)实现(迁移)个人帐户。
    如果为“否”,那么您仍然应该考虑个人帐户的迁移(即使没有新产品)。

PS:


它并不总是描述特定任务中的明确解决方案,并且并非所有解决方案都需要应用。

描述它们并不是为了说“像我一样做”。 例如,可以使yii1的cookie自动进入yii2自动登录,但是,说得通一点也不好(并且应该以某种方式证明这种拐杖是合理的)。

但是我已经花了一些时间来逐步进行项目迁移,如果有人根据我的经验将他救出来,我会感到很高兴。

Source: https://habr.com/ru/post/zh-CN419999/


All Articles