Миграция пользователей на безопасный алгоритм хеширования в Symfony

Ваше приложение может использовать старый, небезопасный алгоритм хеширования для хранения пароля, такой как MD5 (без использования соли)

Эта статья объясняет как преобразовать уже имеющиеся пароли, зашифрованные уязвимым алгоритмом в пароли зашифрованные с использованием безопасного метода хеширования (например с использованием Bcrypt )

Что бы решить проблему, мы сделаем конвертацию на лету, когда пользователь успешно входит в систему. Будем использовать интерфейс EncoderAwareInterface

login listener и использовать не очень хорошо известные параметры в security.yml.

 

Аутентификация до миграции

Если ваше приложение использует зашифрованные при помощи MD5 пароли, файл security.yml будет выглядеть примерно так что бы аутентификация Symfony работала

# app/config/security.yml
security:  
    encoders:
        AppBundle\Entity\User:
            algorithm: md5
            encode_as_base64: false
            iterations:       1

В этой статье, мы предполагаем что сущность User выглядит следующим образом:

# src/AppBundle/Entity/User.php
<!--?php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Table(name="app_users") * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository") */ class User implements UserInterface, \Serializable { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string", length=25, unique=true) */ private $username; /** * @ORM\Column(type="string", length=64) */ private $password; public function getId() { return $this->id;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function setPassword($password)
    {
        $this->password = $password;

        return $this;
    }

    public function getSalt()
    {
        return null;
    }

    // ...
}
</pre>
<h4>Подготовка базы данных</h4>
<p>Мы будем использовать разные поля для каждого метода шифрования:</p>
<ul>
<li>Переименуем поле <code>password</code> в <code>old_password</code>.</li>
<li>Добавим новое поле <code>password</code>, которое будет содержать закодированный по-новому пароль.</li>
<li>Сделаем оба поля не обязательными для заполнения.</li>
</ul>
<p>Новая сущность  <code>User</code> будет выглядеть следующий образом:</p>
<pre># src/AppBundle/Entity/User.php
class User implements UserInterface, EncoderAwareInterface, \Serializable  
{
    // ...

    /**
     * @ORM\Column(type="string", length=64, nullable=true)
     */
    private $oldPassword;

    /**
     * @ORM\Column(type="string", length=64, nullable=true)
     */
    private $password;

    public function getPassword()
    {
        return null === $this-&gt;password ? $this-&gt;oldPassword : $this-&gt;password;
    }

    // ...
}
</pre>
<h4>Делаем что бы авторизация работала с двумя алгоритмами хеширования</h4>
<p>Сконфигурируем два Encoder'а:</p>
<ul>
<li>Новый encoder, который будет по-умолчанию для сущности  <code>User</code>  (следовательно ключ <code>AppBundle\Entity\User</code>).</li>
<li>Другой используется для пользователей которые еще не мигрировали (используют MD5), называется <code>legacy_encoder</code>.</li>
</ul>
<p>Определим эти шифровальщики в файле <code>security.yml</code>:</p>
<pre># app/config/security.yml
security:  
    encoders:
        AppBundle\Entity\User:
            algorithm: bcrypt
        legacy_encoder:
            algorithm:        md5
            encode_as_base64: false
            iterations:       1</pre>
<p>В свою очередь, что бы указать Symfony какой из шифровальщиков использовать в зависимости от пользователя который входит в систему, мы будем использовать the <code>EncoderAwareInterface</code> для сущности  <code>User</code>, с методом <code>getEncoderName()</code>:</p>
<pre>
# src/AppBundle/Entity/User.php
<?php // ... use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface; class User implements UserInterface, EncoderAwareInterface, \Serializable { // ... /** * Tells whether user uses the legacy password encoding or the new one * * @return boolean */ public function hasLegacyPassword() { return null !== $this->oldPassword;
    }

    /**
     * {@inheritDoc}
     */
    public function getEncoderName()
    {
        if ($this->hasLegacyPassword()) {
            // User is configured with a legacy password, make use of the legacy encoder
            // configured in security.yml
            return 'legacy_encoder';
        }

        // User is configured with the default password system, make use of the default encoder
        return null;
    }
}
</pre>
<p>Когда сущность реализует этот интерфейс, Symfony будет вызывать метод  the <code>getEncoderName().</code> что бы определить какой из шифровальщиком использовать при проверке пароля.<br ?-->
Все пользователи могут выполнять вход, вне зависимости от используемого алгоритма шифрования.

Добавляем «слушатель», который будет выполнять миграцию

Мы присоединим слушатель на событие Symfony security.interactive_login это событие срабатывает, когда пользователь выполнил успешный вход.

Для начала определим этот слушатель в файле services.yml file:

Declare first the listener in the services.yml file:

# app/config/services.yml
services:  
    app.login_listener:
        class: AppBundle\EventListener\LoginListener
        tags:
            - { name: kernel.event_listener, event: security.interactive_login }
        arguments:
            - "@security.encoder_factory"
            - "@doctrine.orm.entity_manager"

Создадим слушатель

# src/AppBundle/EventListener/LoginListener.php
<?php namespace AppBundle\EventListener; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; class LoginListener { private $encoderFactory; private $om; public function __construct(EncoderFactoryInterface $encoderFactory, ObjectManager $om) { $this->encoderFactory = $encoderFactory;
        $this->om = $om;
    }

    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();
        $token = $event->getAuthenticationToken();

        // Migrate the user to the new hashing algorithm if is using the legacy one
        if ($user->hasLegacyPassword()) {
            // Credentials can be retrieved thanks to the false value of
            // the erase_credentials parameter in security.yml
            $plainPassword = $token->getCredentials();

            $user->setOldPassword(null);
            $encoder = $this->encoderFactory->getEncoder($user);

            $user->setPassword(
                $encoder->encodePassword($plainPassword, $user->getSalt())
            );

            $this->om->persist($user);
            $this->om->flush();
        }

        // We don't need any more credentials
        $token->eraseCredentials();
    }
}

Этот слушатель обновляет пароль пользователь только в случае если пользователь все еще использует устаревшую систему паролей.

Что бы перекодировать пароль, нам нужен незахешированный пароль, введенный пользователем, который по-умолчанию недоступен в  authentication token предоставленный объектом InteractiveLoginEvent. Что бы сделать его доступным, сделайте следующие изменения в файле  security.yml:

# app/config/security.yml
security:  
    erase_credentials: false
    # ...

Как только ваши пользователи будут входить, данные о них будут обновлены в базе данных, делая их более безопасными со временем. Однажды, основная часть пользователей хотя бы раз выполнит вход и вы сможете удалить колонку old_password и реализовать фичу «Забыли пароль?» для тех, кто не был мигирован.

Автор: Michaël Perrin, ссылка на оригинальный пост

Добавить комментарий