
This article does not make sense
without the first part , in which there is an answer “why to do it”.
It is about the method of smooth project migration from yii1 to yii2. Its essence is that the project branches on yii1 and its new version on yii2 work together on the same domain in the same virtual host, and the migration is performed gradually, in small steps (pages, controllers, modules, etc.).
')
The first part was about how to run a bare project on yii2 in an existing virtual host, i.e. make both branches work together without interfering with each other.
After that, the most psychologically difficult stage begins: you need to create a minimal infrastructure for starting. I would highlight 2 tasks: duplicate design and pass-through user authentication.
Duplication of design is given first place for dullness. If you are unlucky, you can simply copy / rearrange the old "1 to 1". Personally, I always combined with the redesign. Those. The interface and design has been significantly updated and in this regard, the work is not stupid. But here to each his own - I pay a lot of attention to the interface and design, on the contrary, someone loves more backend and console. Nevertheless, regardless of preferences, not to pass this task - you have to make the interface, and the amount of work will be quite large.
Pass-through authentication is a bit more interesting, and there will be less work. As in the first article, there will be no revelations here. The nature of the article: tutorial for those who solve this problem for the first time.
If this is your case, then more under the cat
First of all, it is necessary to divide the functionality between the branches. Since it is assumed that the migration stage only at the start, then, most likely, all work with users (registration, authentication, password recovery, etc.) remain on the old site. A new branch on yii2 should simply see already authenticated users.
The yii1 / yii2 authentication mechanisms are slightly different and you need to tweak the yii2 code so that it can see the already authenticated user. Since Authentication data is stored in the session, you just need to agree on the parameters for reading session data.
In the session from yii1, it is stored as follows:
print_r($_SESSION); Array ( [34e60d27092d90364d1807021107e5a3__id] => 123456 [34e60d27092d90364d1807021107e5a3__name] => tester [34e60d27092d90364d1807021107e5a3__states] => Array ( ) )
How do you keep it - check, for example, prefixKey is generated differently in different versions of yii1.
Here is the data you need from yii1
Yii::app()->user->getStateKeyPrefix() Yii::app()->name Yii::app()->getBasePath() Yii::app()->getId() get_class(Yii::app()->user)
The easiest way is to make a test page and show all the necessary data on it - they will be needed later.
Authentication in yii2
In Yii2, all the functionality we need is in the user (
\ yii \ web \ User ) component that controls the authentication state.
- In its getIdentity () method, renewAuthStatus () is called, in which the authentication session is searched for by key from the $ idParam variable (by default, the '__id' is stored there);
- The session variable for the key $ idParam stores the user id (for example, from app / model / User ).
The authentication algorithm is described in detail in the official manual .Of course, in yii1 sessions are saved using a different key. Therefore, you need to make it so that yii2 searches for a user ID using the same keys with which it is stored in yii1.
For this:1. Change the class of the user component, which is responsible for managing the state of authentication, to its own, inherited from
yii / web / User in
config / web.php 'components' => [ 'user' => [ 'class' => 'app\models\WebUser', 'identityClass' => 'app\models\User', ], ]
2. Adjust the value of
$ idParam in
app \ models \ WebUser .
public function init() {
Under the spoiler there will be a few methods that somehow emulate this behavior from yii1.
In general, it would be possible to simply copy the original
_keyPrefix (or even
idParam right
away ) from yii1 and not emulate its generation, but then it would be similar to the “copy incomprehensible garbage” instruction.
You can really copy it because _keyPrefix in yii1 is almost static. It depends on the class name of the user component and on the application ID, which in turn is obtained from the location of the application and its name.
If we restrict ourselves only to the task of authentication, then copying the _keyPrefix value significantly reduces the amount of work. But I will have examples for wider use.
The user component (app \ models \ WebUser) namespace app\models; use yii\web\User; class WebUser extends User { public $autoRenewCookie = false; private $_keyPrefix; private $paramsYii1 = [
And additional methods to it (WebUser).
Separated for easy viewing. public function getIdParamYii1() { return $this->getStateKeyPrefix() . '__id'; } public function getStateKeyPrefix() { if ($this->_keyPrefix !== null) return $this->_keyPrefix; $class = $this->paramsYii1['classUserComponent']; return $this->_keyPrefix = md5('Yii.' . $class . '.' . $this->getAppIdYii1()); } public function getAppIdYii1() { if ($this->paramsYii1['appId']) return $this->paramsYii1['appId']; return $this->paramsYii1['appId'] = sprintf('%x', crc32($this->getBasePathYii1() . $this->paramsYii1['appName'])); } private function getBasePathYii1() { $basePath = realpath(\Yii::getAlias('@app') . DIRECTORY_SEPARATOR . $this->paramsYii1['relPath']); if (!$basePath) throw new InvalidConfigException('basePath yii1 .'); return $basePath; }
Only for the “ reconcile session key format ” task, the decomposition of methods is a bit complicated, but they will be useful for the examples below. After that, in the new branch on yii2, recognition of users previously authorized in yii1 begins to work. Ideally, this should be stopped, because the slippery slope begins further.
User login to yii2
After the storage format in the session of the user ID has been agreed upon, it is possible that even “automatically” the Login user will work through yii2.
I think it’s wrong to use the login form on yii2 in combat mode without disabling the corresponding form on yii1, however, the basic functionality will be working after small approvals.Since We believe that while user registration and, accordingly, password hashing, is left in usi1, then for a working login through yii2 we need to make sure that the validation method of the saved password in yii2 can understand what was hashed and saved in Yii1.
Check out what these methods do.
For example, if in Yii2 a conditionally standard User user model validates a password like this:
public function validatePassword($password) { return \Yii::$app->getSecurity()->validatePassword($password, $this->password); }
That, look
validatePassword method
($ password, $ hash) from
yii \ base \ Security (Yii2)
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); }
And if on Yii1, password hashing in the User model is done like this:
public function hashPassword($password) { return CPasswordHelper::hashPassword($password); }
Then compare with
verifyPassword ($ password, $ hash) from
yii \ framework \ utils \ CPasswordHelperhashPassword () 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; }
If the hashing and validation methods are different, then you need to change the validation in
validatePassword () from
app \ model \ User .
From the boxes of the latest versions of the framework, password hashes are Yii1 / Yii2 compatible. But this does not mean that they will be compatible with you or that will coincide in the future. With a high degree of probability, the project hashing methods in Yii1 and validation in the new project on Yii2 will differ.
Autologin in Yii2 for the cookies from yii1
If the branch on Yii2 already knows how to transparently use the authentication data of users from Yii1, then why not set up autologin for cookies?
If you get the idea, then I advise you to give it up. I do not see any compelling reason to include autologin on Yii2 without transferring to work with users on this thread (authentication, first of all). Those. I mean the following case:
User authentication is performed on Yii1, but the Yii2 branch should be able to autologin the cookies stored in Yii1.
To be honest, this is a frank perversion. It will not be easy and elegant to do this, and here lies the line between the conscious and justified task of migration and the invention of unnecessary bicycles.
The difficulty is that in both branches Yii is protected from counterfeit cookies, so it’s difficult to reconcile the methods.
- Components are involved in Yii2: user ( \ yii \ web \ User ), Request , Security + CookieCollection
- In Yii1: CWebUser , CHttpRequest , CSecurityManager , CStatePersister , CCookieCollection
However, the cases are different. Below is an example of how to make autologin with bikes.
In yii2, we are interested in the
getIdentityAndDurationFromCookie () method from
\ yii \ web \ User . In the first line of this method, the correct cookie should be obtained:
$value = Yii::$app->getRequest()->getCookies()->getValue($this->identityCookie['name']);
But it will not, because the
Yii :: $ app-> getRequest () -> getCookies () collection will be empty, because
Request Cookies are loaded with validation into
loadCookies () and, of course, they do not pass.
The easiest way to fork a standard behavior is to rewrite
getIdentityAndDurationFromCookie () . For example:
- Download the desired cookie directly from the $ _COOKIE superglobal, in order not to
break the standard cookie loading mechanism.
The name of the identification cookie is just _keyPrefix, which we are already able to receive (or copied). Therefore, we change the standard $ identityCookie in init () .
- Decrypt the resulting cookie "approximately as in yii1". As you wish. For example, I copied the necessary methods from CSecurityManager .
Below, in fact, the code.
We work in app / models / WebUser1. Set identityCookie name in accordance with yii1
public function init() { $this->idParam = $this->getIdParamYii1();
2. Add two more methods.
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; } protected function getIdIdentityFromCookiesYii1() { if (!isset($_COOKIE[$this->identityCookie['name']])) return null; $cookieValue = $_COOKIE[$this->identityCookie['name']];
The code uses a certain
UtilYii1Security class - this is a modified copy-paste of the necessary methods from the
CSecurityManager , so that on the one hand it looks like the original, but with simplifications. For example, in
CSecurityManager there are several options for HMAC generation (hash-based message authentication code), which depend on the version of php and the presence of mbstring. But since it is known that yii1 works in the same environment as yii2, the task is simplified and, accordingly, the code too.
Since it is clear that a clear crutch is being written here, then you should not try to make it universal and give a good form, it is enough to “sharpen” it under your conditions.
UtilYii1Security.php <?php namespace app\components; use yii\base\Exception; use yii\base\Model; use yii\base\InvalidConfigException; class UtilYii1Security { const STATE_VALIDATION_KEY = 'Yii.CSecurityManager.validationkey'; public $hashAlgorithm = 'sha1'; private $_validationKey; private $basePath; private $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; }
Sequencing
When migrating from yii1 to yii2 in terms of authentication, I followed the following sequence:
- Make transparent user authentication between branches.
Those. so that the branch on yii2 accepts users authenticated in yii1. It is fast, not difficult.
- Transfer authentication (user login) from yii1 to yii2.
At the same time turning it off in the old branch. Notice that after this autologin will stop working in the cookies, since cookies from yii1 are no longer suitable, and new pages on yii2 are still few. - Port to yii2 at least the main page of the site
So that you can use autologin for new cookies stored in yii2.
Having autologin at least on the main one will help to mask the missing autologin on the previous branch.
- Check that yii1 understands authenticated in yii2.
By negotiating session keys.
- Transfer user registration to yii2.
The transfer should be done with the coordination of previously saved password hashes. Maybe save the old hash format or enter a new one, but so that the login understands both types. - Whether to think about whether to add to the users of the site a service that gives yii2 out of the box.
I mean the implementation of the IdentityInterface interface for the User , which allows authentication by token, password recovery, etc. Perhaps you already have the appropriate harness, but suddenly not? Then this is a great option to improve the service with minimal effort.
If “yes”, then this will result in the implementation (migration) of a personal account in yii2 (at least partially).
If “no”, then still think about the migration of your personal account (even without new products).
PS:
There are described not always unambiguous solutions in a specific task and not all of them need to be applied.
They are described not for the purpose of saying “do as I do.” For example, it is possible to do autologin in yii1 in yii2 - this is possible, but, to put it mildly, not good (and such a crutch should be justified by something).
But I have already spent on this time during the step-by-step migration of projects and will be glad if someone, looking at my experience, will save it.