Partage d'authentification Yii1 / yii2

image

Cet article n'a pas de sens sans la première partie , dans laquelle il y a une réponse «pourquoi faire cela».

Il s'agit de la technique de migration en douceur d'un projet de yii1 à yii2. Son essence est que les branches du projet sur yii1 et sa nouvelle version sur yii2 fonctionnent ensemble sur le même domaine dans un hôte virtuel, et la migration se fait progressivement, par petites étapes (à travers les pages, les contrôleurs, les modules, etc.).

La première partie était sur la façon d'exécuter un projet nu sur yii2 dans un hôte virtuel existant, c'est-à-dire faire fonctionner les deux branches sans interférer.

Après cela commence l'étape la plus difficile psychologiquement: vous devez créer une infrastructure minimale pour commencer. Je distinguerais 2 tâches: la conception en double et l'authentification des utilisateurs de bout en bout.

La duplication de la conception est donnée en premier lieu par l'ennui. Si vous n'avez pas de chance, vous pouvez simplement copier / restaurer l'ancien «1 en 1». Personnellement, j'ai toujours combiné avec la refonte. C'est-à-dire l'interface et le design ont été considérablement mis à jour et à cet égard, le travail n'est pas stupide. Mais ici, chacun a le sien - je porte beaucoup d'attention à l'interface et au design, quelqu'un, au contraire, aime davantage le backend et la console. Néanmoins, quelles que soient les préférences, il n'y a aucun moyen de dépasser cette tâche - vous devrez créer une interface et la quantité de travail sera assez importante.

L'authentification de bout en bout est un peu plus intéressante et il y aura moins de travail. Comme dans le premier article, il n'y aura pas de révélations. Caractère de l'article: tutoriel pour ceux qui résolvent ce problème pour la première fois.

Si tel est votre cas, alors plus de détails sous la coupe

Tout d'abord, vous devez diviser la fonctionnalité entre les branches. Parce que Il est entendu que la phase de migration n'est qu'au début, puis, très probablement, tous les travaux avec les utilisateurs (enregistrement, authentification, récupération de mot de passe, etc.) restent sur l'ancien site. Et la nouvelle branche sur yii2 devrait juste voir des utilisateurs déjà authentifiés.

Les mécanismes d'authentification de yii1 / yii2 sont légèrement différents et vous devez modifier le code yii2 pour qu'il puisse voir un utilisateur authentifié. Parce que les données d'authentification sont stockées dans la session, il vous suffit de vous mettre d'accord sur les paramètres de lecture des données de session.

Dans une session de yii1, cela est stocké comme ceci:

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

Comment est-il stocké avec vous - vérifiez, par exemple, que prefixKey est généré différemment dans différentes versions de yii1.

Voici les données dont vous avez besoin de yii1

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

Le moyen le plus simple consiste à créer une page de test et à y afficher toutes les données nécessaires - elles seront nécessaires à l'avenir.

Authentification en yii2


Dans Yii2, toutes les fonctionnalités dont nous avons besoin se trouvent dans le composant utilisateur ( \ yii \ web \ User ), qui contrôle l'état d'authentification.

  • Dans sa méthode getIdentity () , renouvelleAuthStatus () est appelé, dans lequel la session d'authentification est recherchée par la clé de la variable $ idParam (par défaut '__id' y est stocké);
  • Dans la variable de session, l'ID utilisateur est stocké à l'aide de la clé $ idParam (par exemple, depuis app / model / User ).

L'algorithme d'authentification est décrit en détail dans le guide officiel .

Bien sûr, dans yii1, les sessions sont stockées avec une clé différente. Par conséquent, vous devez faire en sorte que yii2 recherche l'ID utilisateur à l'aide des mêmes clés avec lesquelles il est stocké dans yii1.

Pour ce faire:

1. Remplacez la classe du composant utilisateur responsable de la gestion de l'état d'authentification par le nôtre, hérité de yii / web / User dans config / web.php

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

2. Ajustez la valeur de $ idParam dans app \ models \ WebUser .

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

Sous le spoiler, il y aura quelques méthodes qui émuleront en quelque sorte un comportement similaire de yii1.

En général, vous pouvez simplement copier le _keyPrefix original (ou même idParam tout de suite ) à partir de yii1 et ne pas émuler sa génération, mais ce serait comme l'instruction «copier les déchets incompréhensibles».

En effet, vous pouvez copier car _keyPrefix en yii1 est presque statique. Cela dépend du nom de classe du composant utilisateur et de l'ID d'application, qui à son tour est obtenu à partir de l'emplacement de l'application et de son nom.

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

Si nous nous limitons à la tâche d'authentification uniquement, la copie de la valeur _keyPrefix réduit considérablement la quantité de travail. Mais j'aurai des exemples pour une utilisation plus large.

Composant utilisateur (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(); } } 

Et des méthodes supplémentaires (WebUser). Séparé pour une visualisation facile.

 /** *     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; } 

La décomposition des méthodes est un peu compliquée uniquement pour " convenir du format de la clé de session ", mais elles sont utiles pour les exemples ci-dessous.

Après cela, dans une nouvelle branche sur yii2, la reconnaissance des utilisateurs précédemment autorisés dans yii1 commence à fonctionner. Idéalement, il faudrait s'arrêter à cela, car le chemin glissant commence plus loin.

Connexion utilisateur dans yii2


Une fois que le format de stockage de l'ID utilisateur dans la session a été convenu, il est possible que même «automatiquement» la connexion de l'utilisateur fonctionne via yii2.

Je considère qu'il est incorrect d'utiliser le formulaire de connexion sur yii2 en mode combat sans désactiver le formulaire correspondant sur yii1, cependant, la fonctionnalité de base fonctionnera après un peu de coordination.

Parce que Nous pensons que si l'enregistrement des utilisateurs et, par conséquent, le hachage des mots de passe, restent dans yii1, pour une connexion de travail via yii2, nous devons nous assurer que la méthode de validation du mot de passe enregistré dans yii2 peut comprendre ce qui a été haché et stocké dans Yii1.

Vérifiez ce que font ces méthodes.

Par exemple, si dans Yii2, le modèle utilisateur standard conditionnel User valide le mot de passe comme suit:

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

Ensuite, regardez la méthode validatePassword ($ password, $ hash) de 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); } 

Et si le hachage de mot de passe Yii1 dans le modèle utilisateur se fait comme ceci:

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

Comparez ensuite avec verifyPassword ($ password, $ hash) de yii \ framework \ utils \ CPasswordHelper

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


Si les méthodes de hachage et de validation sont différentes, vous devez modifier la validation dans validatePassword () à partir de app \ model \ User .

À partir des boîtes des dernières versions du cadre, les hachages de mot de passe Yii1 / Yii2 sont compatibles. Mais cela ne signifie pas du tout qu'ils seront compatibles avec vous ou qu'ils coïncideront à l'avenir. Avec un degré de probabilité élevé, les méthodes de hachage du projet dans Yii1 et la validation dans le nouveau projet sur Yii2 seront différentes.

Connexion automatique dans Yii2 sur les cookies de yii1


Puisqu'une branche sur Yii2 sait déjà comment utiliser de manière transparente les données d'authentification des utilisateurs de Yii1, pourquoi ne pas configurer la connexion automatique par cookie?
Si une telle pensée vous arrive, alors je vous conseille de l'abandonner. Je ne vois aucune bonne raison d'activer la connexion automatique sur Yii2 sans transférer le travail de l'utilisateur (authentification, tout d'abord) sur ce fil. C'est-à-dire Je veux dire le cas suivant:

l'authentification des utilisateurs est effectuée sur Yii1, mais la branche Yii2 devrait être en mesure de s'authentifier sur les cookies stockés dans Yii1.

Pour être honnête, c'est une perversion franche. Ce ne sera pas facile et élégant à faire, et juste ici la ligne passe entre la tâche consciente et justifiée de la migration et l'invention des vélos inutiles.

La difficulté est que dans les deux branches Yii est protégé contre les faux cookies, il est donc difficile de s'entendre sur les méthodes.

  • Les composants impliqués dans Yii2 sont: utilisateur ( \ yii \ web \ User ), Request , Security + CookieCollection
  • Dans Yii1: CWebUser , CHttpRequest , CSecurityManager , CStatePersister , CCookieCollection

Cependant, les cas sont différents. Vous trouverez ci-dessous un exemple de création d'une connexion automatique avec des vélos.

Dans yii2, nous nous intéressons à la méthode getIdentityAndDurationFromCookie () de \ yii \ web \ User . Dans la première ligne de cette méthode, le cookie souhaité doit être obtenu:

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

Mais elle ne le sera pas, car la collection Yii :: $ app-> getRequest () -> getCookies () sera vide, car dans Request les cookies sont chargés avec validation dans loadCookies () et, bien sûr, cela ne passe pas.

Le moyen le plus simple de bifurquer est le comportement standard en écrasant getIdentityAndDurationFromCookie () . Par exemple, comme ceci:

  1. Téléchargez le cookie souhaité directement depuis le superglobal $ _COOKIE afin de ne pas casser adapter le mécanisme standard de chargement des cookies.

    Le nom du cookie d'identification est simplement _keyPrefix, que nous savons déjà recevoir (ou copier). Par conséquent, nous modifions le $ identityCookie standard dans init () .
  2. Déchiffrez le cookie résultant "à peu près comme dans yii1". Comme vous le souhaitez. Par exemple, j'ai copié les méthodes requises depuis CSecurityManager .

Ci-dessous, en fait, se trouve le code.

Nous travaillons en application / modèles / WebUser
1. Le nom des cookies cookie d'identité définis conformément à yii1

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


2. Ajoutez deux autres méthodes

 /** *    */ 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; } 


Le code utilise une certaine classe UtilYii1Security - il s'agit d'un copier-coller modifié des méthodes nécessaires de CSecurityManager , de sorte qu'il ressemble d'une part à l'original, mais avec des simplifications. Par exemple, dans CSecurityManager, il existe plusieurs options pour générer HMAC (code d'authentification de message basé sur le hachage), qui dépendent de la version php et de la présence de mbstring. Mais depuis Il est connu que yii1 fonctionne dans le même environnement que yii2, alors la tâche est simplifiée et, en conséquence, le code aussi.

Parce que il est bien clair qu'une béquille explicite est écrite ici, alors pas besoin d'essayer de la rendre universelle et de lui donner une forme gracieuse, il suffit de "l'emprisonner" dans vos conditions.

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'); } } 


Séquence d'actions


Lors de la migration de yii1 vers yii2 en termes d'authentification, j'ai suivi la séquence suivante:

  1. Rendez l'authentification utilisateur transparente entre les branches.
    C'est-à-dire afin que la branche yii2 accepte les utilisateurs authentifiés avec yii1. C'est rapide, pas difficile.
  2. Transférez l'authentification (connexion utilisateur) de yii1 à yii2.
    Désactiver en même temps dans l'ancienne branche. Notez qu'après cela, la connexion automatique pour les cookies cessera de fonctionner, car les cookies de yii1 ne sont plus adaptés, et il y a encore peu de nouvelles pages sur yii2.
  3. Portez au moins la page principale du site vers yii2
    Afin que vous puissiez utiliser la connexion automatique pour les nouveaux cookies stockés dans yii2.
    La présence d'une connexion automatique, au moins sur la connexion principale, aidera à masquer la connexion automatique manquante sur la branche précédente.
  4. Vérifiez que yii1 comprend authentifié dans yii2.
    Par la négociation des clés de session.
  5. Transférez l'enregistrement de l'utilisateur vers yii2.
    Le transfert doit être effectué avec la coordination des hachages de mot de passe précédemment stockés. Peut-être conserver l'ancien format de hachage ou en introduire un nouveau, mais pour que la connexion comprenne les deux types.
  6. Pensez à ajouter des utilisateurs au site, un service qui donne yii2 «prêt à l'emploi».
    Je veux dire la mise en œuvre de l'interface IdentityInterface de l' utilisateur, qui permet l'authentification par jeton, la récupération de mot de passe, etc. Peut-être avez-vous déjà le harnais approprié, mais tout à coup non? C'est alors une excellente option pour améliorer le service avec un minimum d'effort.

    Si oui, cela conduira à la mise en œuvre (migration) du compte personnel en yii2 (au moins partiellement).
    Si «non», alors vous devriez toujours penser à la migration de votre compte personnel (même sans nouveaux produits).

PS:


Il ne décrit pas toujours des solutions non ambiguës dans une tâche spécifique et toutes n'ont pas besoin d'être appliquées.

Ils ne sont pas décrits dans le but de dire «fais comme moi». Par exemple, faire l'autologinisation yii2 pour les cookies à partir de yii1 est possible, mais, pour le dire légèrement, pas bon (et une telle béquille devrait être justifiée d'une manière ou d'une autre).

Mais j'ai déjà consacré du temps à cette migration pas à pas des projets et je serai content si quelqu'un, au vu de mon expérience, le sauve.

Source: https://habr.com/ru/post/fr419999/


All Articles