Compartilhamento de autenticação Yii1 / yii2

imagem

Este artigo não faz sentido sem a primeira parte , na qual há uma resposta "por que fazer isso".

É sobre a técnica de migrar sem problemas um projeto de yii1 para yii2. Sua essência é que os ramos do projeto no yii1 e sua nova versão no yii2 trabalham juntos no mesmo domínio em um host virtual, e a migração é realizada gradualmente, em pequenas etapas (através de páginas, controladores, módulos etc.).

A primeira parte foi sobre como executar um projeto simples no yii2 em um host virtual existente, ou seja, faça os dois ramos trabalharem juntos sem interferir um no outro.

Depois disso, começa a fase mais psicologicamente difícil: você precisa criar uma infraestrutura mínima para o início. Eu destacaria duas tarefas: design duplicado e autenticação de usuário de ponta a ponta.

A duplicação do design é dada em primeiro lugar pelo chato. Se você tiver azar, basta copiar / reverter o antigo "1 em 1". Pessoalmente, eu sempre combinei com o redesenho. I.e. a interface e o design foram significativamente atualizados e, nesse sentido, o trabalho não é estúpido. Mas aqui cada um na sua - presto muita atenção à interface e design, alguém, pelo contrário, gosta mais do back-end e do console. No entanto, independentemente das preferências, não há como superar essa tarefa - você precisará fazer uma interface e a quantidade de trabalho será bastante grande.

A autenticação de ponta a ponta é um pouco mais interessante e haverá menos trabalho. Como no primeiro artigo, não haverá revelações. Personagem do artigo: tutorial para quem resolve esse problema pela primeira vez.

Se este for o seu caso, mais detalhes serão apresentados

Primeiro de tudo, você precisa dividir a funcionalidade entre os ramos. Porque Entende-se que o estágio de migração ocorre apenas no início e, provavelmente, todo o trabalho com usuários (registro, autenticação, recuperação de senha etc.) permanece no site antigo. E a nova ramificação no yii2 deve apenas ver usuários já autenticados.

Os mecanismos de autenticação do yii1 / yii2 são um pouco diferentes e você precisa ajustar o código do yii2 para que ele possa ver um usuário autenticado. Porque Como os dados de autenticação são armazenados na sessão, você só precisa concordar com os parâmetros para a leitura dos dados da sessão.

Em uma sessão do yii1, isso é armazenado da seguinte forma:

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

Como ele é armazenado com você - verifique, por exemplo, o prefixoKey é gerado de maneira diferente em diferentes versões do yii1.

Aqui estão os dados que você precisa do yii1

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

A maneira mais fácil é criar uma página de teste e mostrar nela todos os dados necessários - eles serão necessários no futuro.

Autenticação no yii2


No Yii2, toda a funcionalidade que precisamos está localizada no componente do usuário ( \ yii \ web \ User ), que controla o estado da autenticação.

  • No método getIdentity () , renewAuthStatus () é chamado, no qual a sessão de autenticação é procurada pela chave da variável $ idParam (por padrão, '__id' é armazenado lá);
  • Na variável da sessão, o ID do usuário é armazenado usando a chave $ idParam (por exemplo, de app / model / User ).

O algoritmo de autenticação é descrito em detalhes no guia oficial .

Obviamente, no yii1, as sessões são armazenadas com uma chave diferente. Portanto, você precisa fazer o yii2 procurar o ID do usuário usando as mesmas chaves com as quais ele é armazenado no yii1.

Para fazer isso:

1. Altere a classe do componente de usuário responsável por gerenciar o estado de autenticação para o nosso, herdado de yii / web / User em config / web.php

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

2. Ajuste o valor de $ idParam em app \ models \ WebUser .

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

Sob o spoiler, haverá alguns métodos que, de alguma forma, imitam o comportamento semelhante do yii1.

Em geral, pode-se copiar o _keyPrefix original (ou mesmo o idParam imediatamente ) do yii1 e não emular sua geração, mas seria como a instrução "copiar lixo incompreensível".

Na verdade, você pode copiar porque _keyPrefix no yii1 é quase estático. Depende do nome da classe do componente do usuário e do ID do aplicativo, que por sua vez é obtido a partir do local do aplicativo e seu nome.

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

Se nos limitarmos apenas à tarefa de autenticação, copiar o valor _keyPrefix reduzirá significativamente a quantidade de trabalho. Mas terei exemplos para uso mais amplo.

Componente do usuário (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(); } } 

E métodos adicionais para ele (WebUser). Separado para facilitar a visualização.

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

Somente para a tarefa de " concordar com o formato da chave da sessão ", a decomposição dos métodos é um pouco complicada, mas eles são úteis para os exemplos abaixo.

Depois disso, em uma nova ramificação no yii2, o reconhecimento de usuários previamente autorizados no yii1 começa a funcionar. Idealmente, seria necessário parar com isso, porque o caminho escorregadio começa ainda mais.

Login de usuário no yii2


Depois que o formato de armazenamento para o ID do usuário na sessão for acordado, é possível que mesmo "automaticamente" o login do Usuário funcione com o yii2.

Considero incorreto usar o formulário de login no yii2 no modo de combate sem desativar o formulário correspondente no yii1; no entanto, a funcionalidade básica funcionará após uma pequena coordenação.

Porque Acreditamos que, enquanto o registro do usuário e, consequentemente, o hash da senha, permanecem no yii1, para um login de trabalho no yii2, precisamos garantir que o método de validação da senha salva no yii2 possa entender o que foi armazenado e armazenado no yii1.

Verifique o que esses métodos fazem.

Por exemplo, se no Yii2, o modelo de usuário condicionalmente padrão User valida a senha da seguinte maneira:

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

Em seguida, observe o método validatePassword ($ password, $ hash) em 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); } 

E se no Yii1 o hash de senha no modelo de Usuário for feito assim:

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

Em seguida, compare com o confirmPassword ($ senha, $ hash) em 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; } 


Se os métodos de hash e validação forem diferentes, será necessário alterar a validação em validatePassword () de app \ model \ User .

Nas caixas das versões mais recentes da estrutura, os hashes de senha Yii1 / Yii2 são compatíveis. Mas isso não significa que eles sejam compatíveis com você ou que coincidam no futuro. Com um alto grau de probabilidade, os métodos de hash do projeto no Yii1 e a validação no novo projeto no Yii2 serão diferentes.

Login automático no Yii2 em cookies da yii1


Como uma filial no Yii2 já sabe como usar com transparência os dados de autenticação do usuário do Yii1, por que não configurar o logon automático por cookie?
Se tal pensamento acontecer, aconselho-o a abandoná-lo. Não vejo uma boa razão para ativar o logon automático no Yii2 sem transferir o trabalho do usuário (autenticação, antes de tudo) para este segmento. I.e. Quero dizer o seguinte caso:

a autenticação do usuário é realizada no Yii1, mas a ramificação do Yii2 deve poder fazer logon automático nos cookies armazenados no Yii1.

Para ser sincero, é uma perversão franca. Não será fácil e elegante, e apenas aqui a linha passa entre a tarefa consciente e justificada da migração e a invenção de bicicletas desnecessárias.

A dificuldade é que, nos dois ramos, o Yii está protegido contra cookies falsos, por isso é difícil concordar com os métodos.

  • Os componentes envolvidos no Yii2 são: usuário ( \ yii \ web \ User ), Request , Security + CookieCollection
  • No Yii1: CWebUser , CHttpRequest , CSecurityManager , CStatePersister , CCookieCollection

No entanto, os casos são diferentes. Abaixo está um exemplo de como fazer um login automático com bicicletas.

No yii2, estamos interessados ​​no método getIdentityAndDurationFromCookie () de \ yii \ web \ User . Na primeira linha deste método, o cookie desejado deve ser obtido:

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

Mas ela não será, porque a coleção Yii :: $ app-> getRequest () -> getCookies () ficará vazia, porque em Request os cookies são carregados com validação em loadCookies () e, é claro, não passa.

A maneira mais fácil de bifurcar é o comportamento padrão substituindo getIdentityAndDurationFromCookie () . Por exemplo, assim:

  1. Faça o download do cookie desejado diretamente do superglobal $ _COOKIE para não adaptar o mecanismo padrão de carregamento de cookies.

    O nome do cookie de identificação é apenas _keyPrefix, que já sabemos como receber (ou copiar). Portanto, alteramos o $ identityCookie padrão em init () .
  2. Descriptografe o cookie resultante "aproximadamente como em yii1". Como você deseja. Por exemplo, copiei os métodos necessários do CSecurityManager .

Abaixo, de fato, está o código.

Trabalhamos em app / models / WebUser
1. O nome identityCookie cookies definido de acordo com yii1

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


2. Adicione mais dois métodos

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


O código usa uma certa classe UtilYii1Security - esta é uma cópia e colar modificada dos métodos necessários do CSecurityManager , para que, por um lado, pareça com o original, mas com simplificações. Por exemplo, no CSecurityManager, existem várias opções para gerar o HMAC (código de autenticação de mensagens baseado em hash), que depende da versão do php e da presença do mbstring. Mas desde Sabe-se que o yii1 funciona no mesmo ambiente que o yii2, então a tarefa é simplificada e, consequentemente, o código também.

Porque é bastante claro que uma muleta explícita está escrita aqui, então não há necessidade de tentar universalizá-la e dar-lhe uma forma graciosa, é suficiente para "aprisioná-la" sob suas condições.

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


Sequência de ações


Ao migrar do yii1 para o yii2 em termos de autenticação, segui a seguinte sequência:

  1. Faça autenticação de usuário transparente entre filiais.
    I.e. para que a ramificação yii2 aceite usuários autenticados com yii1. É rápido, não é difícil.
  2. Transfira a autenticação (login do usuário) de yii1 para yii2.
    Desabilitando-o ao mesmo tempo na ramificação antiga. Observe que, depois disso, o login automático de cookies deixará de funcionar, porque os cookies do yii1 não são mais adequados e ainda existem poucas páginas novas no yii2.
  3. Portar pelo menos a página principal do site para yii2
    Para que você possa usar o logon automático para os novos cookies armazenados no yii2.
    A presença de um logon automático, pelo menos no principal, ajudará a mascarar o logon ausente na ramificação anterior.
  4. Verifique se o yii1 entende autenticado no yii2.
    Através da negociação de chaves de sessão.
  5. Transfira o registro do usuário para yii2.
    A transferência deve ser feita com a coordenação dos hashes de senha armazenados anteriormente. Talvez mantenha o formato hash antigo ou introduza um novo, mas para que o login entenda os dois tipos.
  6. Considere adicionar usuários ao site um serviço que fornece ao yii2 "pronto para uso".
    Refiro-me à implementação da interface IdentityInterface do usuário, que permite autenticação por token, recuperação de senha etc. Talvez você já tenha o cinto adequado, mas de repente não? Então essa é uma ótima opção para melhorar o serviço com o mínimo de esforço.

    Se sim, isso levará à implementação (migração) da conta pessoal no yii2 (pelo menos parcialmente).
    Se "não", você ainda deve pensar na migração da sua conta pessoal (mesmo sem novos produtos).

PS:


Nem sempre descreve soluções inequívocas em uma tarefa específica e nem todas precisam ser aplicadas.

Eles não são descritos com o objetivo de dizer "faça o que eu faço" Por exemplo, é possível fazer o login automático do yii2 para cookies a partir do yii1, mas, para dizer o mínimo, não é bom (e essa muleta deve ser justificada de alguma forma).

Mas já dediquei tempo a essa migração passo a passo de projetos e ficarei feliz se alguém, olhando minha experiência, o salvar.

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


All Articles