Uso compartido de autenticación Yii1 / yii2

imagen

Este artículo no tiene sentido sin la primera parte , en la que hay una respuesta "por qué hacer esto".

Se trata de la técnica de migrar sin problemas un proyecto de yii1 a yii2. Su esencia es que las ramas del proyecto en yii1 y su nueva versión en yii2 trabajan juntas en el mismo dominio en un host virtual, y la migración se lleva a cabo gradualmente, en pequeños pasos (a través de páginas, controladores, módulos, etc.).

La primera parte fue sobre cómo ejecutar un proyecto simple en yii2 en un host virtual existente, es decir. hacer que ambas ramas trabajen juntas sin interferir entre sí.

Después de eso, comienza la etapa más psicológicamente difícil: debe crear una infraestructura mínima para comenzar. Destacaría 2 tareas: diseño duplicado y autenticación de usuario de extremo a extremo.

La duplicación del diseño se da en primer lugar por aburrida. Si no tiene suerte, simplemente puede copiar / revertir el antiguo "1 en 1". Personalmente, siempre lo combiné con el rediseño. Es decir La interfaz y el diseño se han actualizado significativamente y, en este sentido, el trabajo no es estúpido. Pero aquí cada uno lo suyo: presto mucha atención a la interfaz y el diseño, a alguien, por el contrario, le gusta más el backend y la consola. Sin embargo, independientemente de las preferencias, no hay forma de superar esta tarea: tendrá que hacer una interfaz y la cantidad de trabajo será bastante grande.

La autenticación de extremo a extremo es un poco más interesante y habrá menos trabajo. Como en el primer artículo, no habrá revelaciones. Carácter del artículo: tutorial para quienes resuelven este problema por primera vez.

Si este es tu caso, entonces más detalles debajo del corte

En primer lugar, debe dividir la funcionalidad entre las ramas. Porque Se entiende que la etapa de migración es solo al comienzo, luego, muy probablemente, todo el trabajo con los usuarios (registro, autenticación, recuperación de contraseña, etc.) permanece en el sitio anterior. Y la nueva sucursal en yii2 debería ver usuarios ya autenticados.

Los mecanismos de autenticación de yii1 / yii2 son ligeramente diferentes y debe ajustar el código yii2 para que pueda ver un usuario autenticado. Porque los datos de autenticación se almacenan en la sesión, solo necesita acordar los parámetros para leer los datos de la sesión.

En una sesión de yii1, esto se almacena de alguna manera así:

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

¿Cómo se almacena con usted? Compruebe, por ejemplo, prefixKey se genera de manera diferente en diferentes versiones de yii1.

Aquí están los datos que necesita de yii1

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

La forma más fácil es hacer una página de prueba y mostrar en ella todos los datos necesarios; serán necesarios en el futuro.

Autenticación en yii2


En Yii2, toda la funcionalidad que necesitamos se encuentra en el componente de usuario ( \ yii \ web \ User ), que controla el estado de autenticación.

  • En su método getIdentity (), se llama a refreshAuthStatus () , en el que la clave busca la sesión de autenticación de la variable $ idParam (de forma predeterminada, '__id' se almacena allí);
  • En la variable de sesión, la identificación del usuario se almacena usando la clave $ idParam (por ejemplo, de la aplicación / modelo / Usuario ).

El algoritmo de autenticación se describe en detalle en la guía oficial .

Por supuesto, en yii1, las sesiones se almacenan con una clave diferente. Por lo tanto, debe hacer que yii2 busque la ID de usuario utilizando las mismas claves con las que está almacenada en yii1.

Para hacer esto:

1. Cambie la clase del componente de usuario responsable de administrar el estado de autenticación a la nuestra, heredada de yii / web / User en config / web.php

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

2. Ajuste el valor de $ idParam en app \ models \ WebUser .

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

Debajo del spoiler, habrá algunos métodos que de alguna manera emulan un comportamiento similar de yii1.

En general, uno podría copiar el _keyPrefix original (o incluso idParam de inmediato ) de yii1 y no emular su generación, pero entonces sería como la instrucción "copiar basura incomprensible".

De hecho, puede copiar porque _keyPrefix en yii1 es casi estático. Depende del nombre de la clase del componente del usuario y del ID de la aplicación, que a su vez se obtiene de la ubicación de la aplicación y su nombre.

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

Si nos limitamos solo a la tarea de autenticación, copiar el valor _keyPrefix reduce significativamente la cantidad de trabajo. Pero tendré ejemplos para un uso más amplio.

Componente de usuario (aplicación \ modelos \ Usuario web)
 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(); } } 

Y métodos adicionales (WebUser). Separado para una fácil visualización.

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

Solo para la tarea de " acordar el formato de la clave de sesión " la descomposición de los métodos es un poco complicada, pero son útiles para los ejemplos a continuación.

Después de eso, en una nueva sucursal en yii2, el reconocimiento de usuarios previamente autorizados en yii1 comienza a funcionar. Idealmente, sería necesario detenerse en esto, porque el camino resbaladizo comienza aún más.

Inicio de sesión de usuario en yii2


Después de acordar el formato de almacenamiento para la ID de usuario en la sesión, es posible que incluso "automáticamente" el inicio de sesión del Usuario funcione a través de yii2.

Considero incorrecto utilizar el formulario de inicio de sesión en yii2 en modo de combate sin deshabilitar el formulario correspondiente en yii1, sin embargo, la funcionalidad básica funcionará después de un poco de coordinación.

Porque Creemos que, si bien el registro de usuario y, en consecuencia, el hash de contraseña, permanece en yii1, para un inicio de sesión que funcione a través de yii2, debemos asegurarnos de que el método de validación de la contraseña guardada en yii2 pueda comprender lo que se ha hash y almacenado en Yii1.

Comprueba qué hacen estos métodos.

Por ejemplo, si en Yii2, el modelo de usuario condicionalmente estándar Usuario valida la contraseña de la siguiente manera:

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

Luego, mire el método validatePassword ($ contraseña, $ 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); } 

Y si en Yii1, el hash de contraseña en el modelo de Usuario se hace así:

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

Luego compárelo con VerifiedPassword ($ contraseña, $ 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 los métodos de hashing y validación son diferentes, entonces debe cambiar la validación en validatePassword () desde app \ model \ User .

De los cuadros de las últimas versiones del marco, los hash de contraseña Yii1 / Yii2 son compatibles. Pero esto no significa en absoluto que sean compatibles con usted o que coincidan en el futuro. Con un alto grado de probabilidad, los métodos de hash del proyecto en Yii1 y la validación en el nuevo proyecto en Yii2 serán diferentes.

Inicio de sesión automático en Yii2 en cookies de yii1


Dado que una sucursal en Yii2 ya sabe cómo usar de manera transparente los datos de autenticación de usuario de Yii1, ¿por qué no configurar el inicio de sesión automático por cookie?
Si tal pensamiento te sucede, entonces te aconsejo que lo abandones. No veo una buena razón para habilitar el inicio de sesión automático en Yii2 sin transferir el trabajo del usuario (autenticación, en primer lugar) a este hilo. Es decir Me refiero al siguiente caso:

la autenticación del usuario se realiza en Yii1, pero la rama Yii2 debería poder iniciar sesión automáticamente en las cookies almacenadas en Yii1.

Para ser honesto, esta es una perversión franca. No será fácil y elegante de hacer, y justo aquí la línea pasa entre la tarea consciente y justificada de la migración y la invención de bicicletas innecesarias.

La dificultad es que en ambas ramas Yii está protegido de cookies falsas, por lo que es difícil acordar métodos.

  • Los componentes involucrados en Yii2 son: usuario ( \ yii \ web \ User ), Solicitud , Seguridad + Colección de cookies
  • En Yii1: CWebUser , CHttpRequest , CSecurityManager , CStatePersister , CCookieCollection

Sin embargo, los casos son diferentes. A continuación se muestra un ejemplo de cómo iniciar sesión en un automóvil con bicicletas.

En yii2, estamos interesados ​​en el método getIdentityAndDurationFromCookie () de \ yii \ web \ User . En la primera línea de este método, se debe obtener la cookie deseada:

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

Pero ella no lo será, porque la colección Yii :: $ app-> getRequest () -> getCookies () estará vacía, porque en Solicitud las cookies se cargan con validación en loadCookies () y, por supuesto, no pasa.

La forma más fácil de bifurcar es el comportamiento estándar sobrescribiendo getIdentityAndDurationFromCookie () . Por ejemplo, así:

  1. Descargue la cookie deseada directamente desde el superglobal $ _COOKIE para no romper la adaptación del mecanismo estándar de carga de cookies.

    El nombre de la cookie de identificación es solo _keyPrefix, que ya sabemos cómo recibir (o copiar). Por lo tanto, cambiamos el $ identityCookie estándar en init () .
  2. Descifre la cookie resultante "aproximadamente como en yii1". Como desees. Por ejemplo, copié los métodos necesarios de CSecurityManager .

Debajo, de hecho, está el código.

Trabajamos en app / models / WebUser
1. El nombre de identidad de cookies de cookies establecido de acuerdo con yii1

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


2. Agregar dos métodos más

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


El código usa una cierta clase de UtilYii1Security : esta es una copia modificada de los métodos necesarios de CSecurityManager , por lo que, por un lado, se parece al original, pero con simplificaciones. Por ejemplo, en CSecurityManager hay varias opciones para generar HMAC (código de autenticación de mensajes basado en hash), que dependen de la versión de php y la presencia de mbstring. Pero desde Se sabe que yii1 funciona en el mismo entorno que yii2, entonces la tarea se simplifica y, en consecuencia, el código también.

Porque está bastante claro que aquí se escribe una muleta explícita, entonces no hay necesidad de intentar que sea universal y darle una forma elegante, es suficiente para "encarcelarla" bajo sus condiciones.

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


Secuencia de acciones


Al migrar de yii1 a yii2 en términos de autenticación, seguí la siguiente secuencia:

  1. Realice una autenticación de usuario transparente entre sucursales.
    Es decir para que la rama yii2 acepte usuarios autenticados con yii1. Es rápido, no difícil.
  2. Transfiera la autenticación (inicio de sesión del usuario) de yii1 a yii2.
    Inhabilitándolo al mismo tiempo en la antigua rama. Tenga en cuenta que después de esto, el inicio de sesión automático para cookies dejará de funcionar, porque las cookies de yii1 ya no son adecuadas, y todavía hay pocas páginas nuevas en yii2.
  3. Puerto al menos la página principal del sitio a yii2
    Para que pueda usar el inicio de sesión automático para las nuevas cookies almacenadas en yii2.
    La presencia de un inicio de sesión automático, al menos en el principal, ayudará a enmascarar el inicio de sesión que falta en la rama anterior.
  4. Compruebe que yii1 entiende autenticado en yii2.
    Mediante negociación de claves de sesión.
  5. Transfiere el registro de usuario a yii2.
    La transferencia debe hacerse con la coordinación de los hashes de contraseñas previamente almacenados. Tal vez conserve el antiguo formato hash o introduzca uno nuevo, pero para que el inicio de sesión comprenda ambos tipos.
  6. Considere agregar a los usuarios al sitio un servicio que ofrezca yii2 "listo para usar".
    Me refiero a la implementación de la interfaz IdentityInterface del usuario, que permite la autenticación por token, recuperación de contraseña, etc. Tal vez ya tienes el arnés apropiado, pero de repente no? Entonces esta es una gran opción para mejorar el servicio con un mínimo esfuerzo.

    En caso afirmativo, esto conducirá a la implementación (migración) de la cuenta personal en yii2 (al menos parcialmente).
    Si responde "no", debe pensar en la migración de su cuenta personal (incluso sin nuevos productos).

PD:


No siempre describe soluciones inequívocas en una tarea específica y no todas deben aplicarse.

No se describen con el propósito de decir "haz lo que yo hago". Por ejemplo, hacer yii2 autologin para cookies de yii1 es posible, pero, por decirlo suavemente, no es bueno (y tal muleta debería estar justificada de alguna manera).

Pero ya he dedicado tiempo a esta migración paso a paso de proyectos y me alegrará que alguien, mirando mi experiencia, lo salve.

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


All Articles