
Блог
Symfony2 + FOSUserBundle. Перенос пользователей с изменением процедуры шифрования пароля.
Есть проект, который вы хотите перенести на Symfony2. Перед вами стоит задача переноса пользователей из старой базы в новую. Вы можете столкнутся с проблемой шифрования паролей.
Какие есть варианты решения?
Вариант 1
Меняем всем пользователям пароли и делаем рассылку уведомлений.
Плюсы:
- Подходил для задач любой сложности (алгоритм шифрования неизвестен).
Минусы:
- Необходимо объяснить клиентку, а ему это может не понравится.
- Пользователи будут не в восторге.
- Реализовать процедуру изменения пароля.
- Реализовать рассылку писем.
Вариант 2
О нем ниже ))
Плюсы:
- Нет минусов первого варианта.
Минусы:
- Надо знать алгоритм шифрования.
Далее о том, как красиво реализовать в 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 за помощь.

