Блог

Symfony2 + FOSUserBundle. Перенос пользователей с изменением процедуры шифрования пароля.

25.11.11   |   symfony2, security, encoder,
+7 (15)

Есть проект, который вы хотите перенести на Symfony2. Перед вами стоит задача переноса пользователей из старой базы в новую. Вы можете столкнутся с проблемой шифрования паролей.

Какие есть варианты решения?

Вариант 1
Меняем всем пользователям пароли и делаем рассылку уведомлений.

Плюсы:

  1. Подходил для задач любой сложности (алгоритм шифрования неизвестен).

Минусы:

  1. Необходимо объяснить клиентку, а ему это может не понравится.
  2. Пользователи будут не в восторге.
  3. Реализовать процедуру изменения пароля.
  4. Реализовать рассылку писем.

Вариант 2
О нем ниже ))

Плюсы:

  1. Нет минусов первого варианта.

Минусы:

  1. Надо знать алгоритм шифрования.

 

Далее о том, как красиво реализовать в Symfony2. Дополнительно покажу как создать свою процедуру шифрования и переопределить обработчик успешной аутентификации(пригодится для некоторых задач).

В Symfony2 за аутентификацию и авторизация отвечает компонент Security, читаем на английском и на русском. Он позволяет выбрать одну из встроенных процедур шифрования или создать свою. Для нас это важно!

На стандартный компонент я навесил FOSUserBundle, который решает задачи: авторизации, регистрации, восстановления пароля, logout, просмотр профиля и еще некоторые. И он добавляет нашей сущности User важное свойство algorithm, которое позволит хранить в базе пароли зашифрованные разными алгоритмами.

 

Ближе к коду.

Для начала нам необходимо создать свою процедуру шифрования.
По умолчанию используется Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder
Его и будем расширять.

Реализуем свой Acme\UserBundle\Security\Encoder\MessageDigestPasswordEncoder

 
<?php

namespace Acme\UserBundle\Security\Encoder;

use Symfony\Component\Security\Core\Encoder;


class MessageDigestPasswordEncoder extends Encoder\BasePasswordEncoder
{
    private $algorithm;
    private $encodeHashAsBase64;

    /**
     * Constructor.
     *
     * @param string  $algorithm          The digest algorithm to use
     * @param Boolean $encodeHashAsBase64 Whether to base64 encode the password hash
     * @param integer $iterations         The number of iterations to use to stretch the password hash
     */
    public function __construct($algorithm = 'sha512', $encodeHashAsBase64 = true, $iterations = 5000)
    {
        $this->algorithm = $algorithm;
        $this->encodeHashAsBase64 = $encodeHashAsBase64;
        $this->iterations = $iterations;
    }

    public function encodePassword($raw, $salt)
    {
        if (!in_array($this->algorithm, hash_algos(), true)) {
            throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
        }

       /*Ключевой момент*/
        if ($this->algorithm === 'sha1') {
            return hash('sha1', $raw);
        }

        $salted = $this->mergePasswordAndSalt($raw, $salt);
        $digest = hash($this->algorithm, $salted, true);

        // "stretch" hash
        for ($i = 1; $i < $this->iterations; $i++) {
            $digest = hash($this->algorithm, $digest.$salted, true);
        }

        return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
    }

    public function isPasswordValid($encoded, $raw, $salt)
    {
        return $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
    }
}

В моем случае, пароли шифровались с помощью sha1.
Важно! В базе для всех пользователей, обновите поле algorithm, установив в него название вашего алгоритма (у меня sha1).

Добавляем новый сервис в service.xml

 
<parameters>
   <parameter key="acme_user.security.encoder.digest.class">
       Acme\UserBundle\Security\Encoder\MessageDigestPasswordEncoder
   </parameter>
</parameters>

<service id="acme_user.security.encoder" class="%acme_user.security.encoder.digest.class%"></service>

В security.yml определяем новую процедуру шифрования для нашей сущности.

 
security:
    encoders:
        Acme\UserBundle\Entity\User: acme_user.security.encoder

Теперь, старые пользователи смогут пройти аутентификацию.

А если у нас в базе пароли хранятся в открытом виде или нас не устраивает алгоритм шифрования и мы хотим надежнее? То читаем ниже )) 
P.S. Еще я хотел привести пароли к стандартному процессу шифрования sha512 + salt + 5000 итераций на хэширование.

Мы можем перешифровать пароли. Для этого, переопределим обработчик успешной аутентификации. Ведь перешифровывать нужно после того, как аутентификация пройдена и у нас есть доступ к паролю.

Создадим свой обработчик
P.S. В нем можно решить задачу редиректа пользователей в зависимости от роли на нужную страницую.


<?php

namespace Acme\UserBundle\Service;

use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;

class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    protected $container;

    protected $security;

    public function __construct(ContainerInterface $container, SecurityContext $security)
    {
        $this->container = $container;
        $this->security = $security;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        $user = $this->security->getToken()->getUser();
        if ($user->getAlgorithm() === 'sha1') {
             $password = $request->get('_password');
             $salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);

             $user->setPlainPassword($password);
             $user->setSalt($salt);

             $userManager = $this->container->get('fos_user.user_manager');
             $userManager->updateUser($user);
        }
        $response = new RedirectResponse('/');

        return $response;
    }

}

По умолчанию, установить свойство salt нельзя. Т.е. $user->setSalt($salt); - выдаст ошибку. Исправляем, добавляя нужный метод в нашу сущность.

Acme\UserBundle\Entity\User


    /**
     * @param $salt should be formed by this formula base_convert(sha1(uniqid(mt_rand(), true)), 16, 36)
     */
    public function setSalt($salt)
    {
        $this->salt = $salt;
    }
 

Добавляем новый сервис в service.xml


<parameters>
    <parameter key="acme_user.service.login_success_handler.class">
        Acme\UserBundle\Service\LoginSuccessHandler
    </parameter>
</parameters>

<service id="acme_user.service.login_success_handler" class="%acme_user.service.login_success_handler.class%">
    <argument type="service" id="service_container"></argument>
    <argument type="service" id="security.context" />
</service>

В security.yml определяем свой обработчик для успешной аутентификации.


firewalls:
        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle #не обязательно
                success_handler: acme_user.service.login_success_handler

Теперь, после прохождения аутентификации, пароли будет перешифрованы на sha512 + salt + 5000 итераций на хэширование (все зависит от настроек).

В итоге, мы все сделали красиво и решили задачу в стиле symfony2 way избавившись от наследия старой базы и увеличили безопасность.

Большое спасибо ondrowan за помощь.

25.11.11   |   naydav
Ответил на форуме (в комменте не очень удобно) http://zendframework.ru/forum/index.php?topic=5387.msg35717#msg35717
16.07.12   |   Александр
Устарела статейка немного. По действующему FOSUserBundle отличаться будет способ.
09.08.12   |   IgorN
Спасибо! Конечно материал устаревает.

Оставить комментарий

Имя*
E-mail* (не публикуется)
Текст сообщения*
Код*