Хеширование и верификация паролей

Дата обновления перевода 2024-07-27

Хеширование и верификация паролей

Большинство приложений используют пароли для допуска пользователей в систему. Эти пароли должны быть хешированы, чтобы храниться безопасно. Компонент Symfony PasswordHasher предоставляет все инструменты, чтобы безопасно хешировать и верифицировать пароли.

Убедитесь в том, что он установлен, выполнив:

1
$ composer require symfony/password-hasher

Конфигурация хешировщика паролей

Перед хешированием паролей, вы должны сконфигурировать хешировщик, используя опцию password_hashers. Вы должны сконфигурировать алгоритм хеширования и, по желанию, некоторые опции алгоритма:

1
2
3
4
5
6
7
8
9
10
11
12
# config/packages/security.yaml
security:
    # ...

    password_hashers:
        # авто-хешировщик с опциями по умолчанию для класса User (и его дочерей)
        App\Entity\User: 'auto'

        # авто-хешировщик с пользовательскими опциями для всех экземпляров PasswordAuthenticatedUserInterface
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
            algorithm: 'auto'
            cost:      15

В данном примере используется алгоритм "авто". Этот хешировщик автоматически выбирает наиболее безопасный алгоритм, доступный в вашей системе. Вместе с миграцией паролей , это позволяет вам всегда максимально обезопашивать ваши пароли (даже при условии появления новых алгоритмов в последующих релизах PHP).

Далее в этой статье вы можете найти полный справочник всех поддерживаемых алгоритмов .

Tip

Хеширование паролей требует большого количества ресурсов и времени, чтобы генерировать безопасные хеши паролей. В общем, это делает ваше хеширование паролей более безопасным.

Однако в тестах безопасные хеши неважны, поэтому вы можете изменить конфигурацию хешировщица паролей в окружении test, чтобы тесты выполнялись быстрее:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# config/packages/test/security.yaml
security:
    # ...

    password_hashers:
        # Используйте ваше имя класса пользователя здесь
        App\Entity\User:
            algorithm: plaintext # отключить хеширование (делайте это только в тестах!)

        # или использовать минимальные возможные значения
        App\Entity\User:
            algorithm: auto # Должно быть то же значение, что и в config/packages/security.yaml
            cost: 4 # Минимальное возможное значение для bcrypt
            time_cost: 3 # Минимальное возможное значение для argon
            memory_cost: 10 # Минимальное возможное значение для argon

Хеширование пароля

После конфигурации правильного алгоритма, вы можете использовать UserPasswordHasherInterface, чтобы хешировать и верифицировать пароли:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/Controller/RegistrationController.php
namespace App\Controller;

// ...
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserController extends AbstractController
{
    public function registration(UserPasswordHasherInterface $passwordHasher): Response
    {
        // ... например, получить данные пользователя из формы регистрации
        $user = new User(...);
        $plaintextPassword = ...;

        // хешировать пароль (основываясь на конфигурации security.yaml для класса $user)
        $hashedPassword = $passwordHasher->hashPassword(
            $user,
            $plaintextPassword
        );
        $user->setPassword($hashedPassword);

        // ...
    }

    public function delete(UserPasswordHasherInterface $passwordHasher, UserInterface $user): void
    {
        // ... например, получить пароль из диалога "подтвердить удаление"
        $plaintextPassword = ...;

        if (!$passwordHasher->isPasswordValid($user, $plaintextPassword)) {
            throw new AccessDeniedHttpException();
        }
    }
}

Сброс пароля

Используя MakerBundle и SymfonyCastsResetPasswordBundle, вы можете создать безопасное, готовое к использованию, решение для обработки забытых паролей. Для начала, установите SymfonyCastsResetPasswordBundle:

1
$ composer require symfonycasts/reset-password-bundle

Затем, используйте команду make:reset-password. Это задаст вам пару вопросов о вашем приложении, и сгенерирует все необходимые вам файлы! После этого, вы увидите сообщение об успехе и список всех шагов, которые вам нужно сделать.

1
$ php bin/console make:reset-password

Tip

Начиная с MakerBundle: v1.57.0 - Вы можете передать --with-uuid или --with-ulid в make:reset-password. Используя Компонент Symfony Uid, сущности будут генерироваться с типом id в виде :ref:df9f8179cf7766fbef42cdab0f1ae769942a64f6int``.

Вы можете настроить поведение пакета сброса пароля, обновив файл reset_password.yaml. Чтобы узнать больше о конфигурации, прочтите руководство SymfonyCastsResetPasswordBundle.

Миграция паролей

Для того, чтобы защитить пароли, рекомендуется хранить их, используя последние алгоритмы хеширования. Это означает, что если в вашей системе поддерживается улучшенный алгоритм хеширования, пароль пользователя должен быть хеширован снова, используя более новый алгоритм, и сохранен. Это возможно с опцией migrate_from:

  1. Сконфигурируйте новый хешировщик, используя "migrate_from"
  2. Обновите пароль
  3. По желанию, Запустите миграцию паролей из пользовательского хешировщика

Сконфигурируйте новый хешировщик, используя "migrate_from"

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# config/packages/security.yaml
security:
    # ...

    password_hashers:
        # хешировщик, используемый ранее для некоторых пользователей
        legacy:
            algorithm: sha256
            encode_as_base64: false
            iterations: 1

        App\Entity\User:
            # новый хешировщик, вместе с его опциями
            algorithm: sodium
            migrate_from:
                - bcrypt # использует хешировщик "bcrypt" с опциями по умолчанию
                - legacy # использует хешировщик "legacy", сконфигурированный выше

С такой настройкой:

  • Новые пользователи будут хешированы с использованием нового алгоритма;
  • Когда в систему войдет пользователь, чей пароль все еще сохранен с помощью старого алгоритма, Symfony верифицирует пароль со старым алгоритмом, а затем обновит пароль, используя новый алгоритм.

Tip

Хешировщики auto, native, bcrypt и argon автоматически включают миграцию паролей, используя следующий список алгоритмов migrate_from:

  1. PBKDF2 (который использует hash_pbkdf2);
  2. Дайджест сообщений (который использует hash)

Оба используют настройку hash_algorithm в качестве алгоритма. Рекомендуется использовать migrate_from вместо hash_algorithm, если только не используется хешировщик auto.

Обновите пароль

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

Вы можете включить поведение обновления пароля, реализовав, как этот новохешированный пароль должен быть сохранен:

После этого, вы закончили, и пароли всегда будут хешированы максимально безопасно!

Note

При использовании компонента PasswordHasher вне приложения Symfony, вы должны вручную использовать метод PasswordHasherInterface::needsRehash(), чтобы проверить необходимо ли повторное хеширование, и метод PasswordHasherInterface::hash(), чтобы повторно хешировать открытый текстовый пароль, используя новый алгоритм.

Обновление пароля при использовании Doctrine

При использовании сущности поставщика пользователей, реализуйте PasswordUpgraderInterface в UserRepository (см. документацию Doctrine, чтобы узнать информацию о том, как создать этот класс, если он еще не создан). Этот интерфейс реализует сохранение новосозданного хеша паролей:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Repository/UserRepository.php
namespace App\Repository;

// ...
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

class UserRepository extends EntityRepository implements PasswordUpgraderInterface
{
    // ...

    public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
    {
        // установить новый хешированный пароль в объекте User
        $user->setPassword($newHashedPassword);

        // выполнить запросы в базе данных
        $this->getEntityManager()->flush();
    }
}

Обновление пароля при использовании пользовательского поставщика пользователей

Если вы используете пользовательского поставщика пользователей , реализуйте PasswordUpgraderInterface в поставщике пользователей:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Security/UserProvider.php
namespace App\Security;

// ...
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
    // ...

    public function upgradePassword(UserInterface $user, string $newHashedPassword): void
    {
        // установить новый хешированный пароль в объекте User
        $user->setPassword($newHashedPassword);

        // ... сохранить новый пароль
    }
}

Запустите миграцию паролей из пользовательского хешировщика

Если вы используете пользовательский хешироващик паролей, вы можете запустить миграцию паролей, вернув true в методе needsRehash():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Security/CustomPasswordHasher.php
namespace App\Security;

// ...
use Symfony\Component\PasswordHasher\PasswordHasherInterface;

class CustomPasswordHasher implements PasswordHasherInterface
{
    // ...

    public function needsRehash(string $hashedPassword): bool
    {
        // проверить, хеширован ли текущий пароль с использованием устаревшего хешировщика
        $hashIsOutdated = ...;

        return $hashIsOutdated;
    }
}

Динамический хешировщики паролей

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

По умолчанию (как показано в начале этой статьи), для App\Entity\User используется алгоритм auto.

Это может быть достаточно безопасно для обычного пользователя, но что, если вы хотите, чтобы у ваших админов был алгоритм сильнее, к примеру, auto с большей стоимостью. Это может быть сделано с помощью именованных хешировщиков:

1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    # ...
    password_hashers:
        harsh:
            algorithm: auto
            cost: 15

Это создает хешировщик с именем harsh. Для того, чтобы экземпляр User использовал его, касс должен реализовать PasswordHasherAwareInterface. Интерфейс требует одного метода - getPasswordHasherName() - который должен возвращать имя хешировщика для использования:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Entity/User.php
namespace App\Entity;

use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class User implements
    UserInterface,
    PasswordAuthenticatedUserInterface,
    PasswordHasherAwareInterface
{
    // ...

    public function getPasswordHasherName(): ?string
    {
        if ($this->isAdmin()) {
            return 'harsh';
        }

        return null; // использовать хешировщик по умолчанию
    }
}

Caution

При миграции паролей , вам не нужно реализовывать PasswordHasherAwareInterface для возврата имени унаследованного хешера: Symfony определит его из вашей конфигурации migrate_from.

Если вы создали собственный хешировщик паролей, реализующий PasswordHasherInterface, вы должны зарегистрировать для него сервис, чтобы использовать его как именованный хешировщик:

1
2
3
4
5
6
# config/packages/security.yaml
security:
    # ...
    password_hashers:
        app_hasher:
            id: 'App\Security\Hasher\MyCustomPasswordHasher'

Это создаст хешировщик с именем app_hasher из сервиса с ID App\Security\Hasher\MyCustomPasswordHasher.

Хеширование отдельностоящей строки

Хешировщик паролей можно использовать для хеширования строк независимо от пользователей. Используя PasswordHasherFactory, вы можете объявить несколько хэшировщиков, получить любой из них по имени и создавать хеши. Затем вы можете проверить, соответствует ли строка заданному хешу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;

// configure different hashers via the factory
$factory = new PasswordHasherFactory([
    'common' => ['algorithm' => 'bcrypt'],
    'sodium' => ['algorithm' => 'sodium'],
]);

// получить хешировщик, используя bcrypt
$hasher = $factory->getPasswordHasher('common');
$hash = $hasher->hash('plain');

// проверить, совпадает ли заданная строка с хешировщиком, вычисленным выше
$hasher->verify($hash, 'invalid'); // false
$hasher->verify($hash, 'plain'); // true

Поддерживаемые алгоритмы

Хешировщик "auto"

Автоматически выбирает лучший доступный хешировщик. Начиная с Symfony 5.3, он использует хешировщик Bcrypt. Если PHP или Symfony в будущем добавит новые хешировщики паролей, он может выбирать другой хешировщик.

Из-за этого, длина хешированных паролей может измениться в будущем, поэтому убедитесь в том, что оставили достаточно места для их сохранения (varchar(255) должно быть хорошей настройкой).

Хешировщик паролей Bcrypt

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

Единственная опция конфигурации - cost, которая является целым числом в диапазоне 4-31 (по умолчанию, 13). Каждый шаг стоимости удваивает время, необходимое для хеширования пароля. Это сделано для того, чтобы сила пароля могла быть адаптирована к последующим улучшениям в мощности вычисления.

Вы можете изменить стоимость в любое время - даже если у вас уже есть пароли, хешированные с использованием другой стоимости. Новые пароли будут хешированы с использованием новой стоимости, в то время, как уже хешированные пароли будут валидированы, используя стоимость, которая была использована во время их хеширования.

Tip

Простая техника для значительного ускорения тестов при использовании BCrypt заключаются в установке стоимости 4, что является минимальным разрешенным значением, в конфигурации окружения test.

Хешировщик паролей Sodium

Использует функцию формирования ключа Argon2. Поддержка Argon2 была представлена в PHP 7.2 путем создания пакета расширения libsodium.

Хешированные пароли имеют длину 96 символов, но в связи с требованиями хеширования, сохраненными в результирующем хеше, это может измениться в будущем, так что убедитесь в том, что оставили достаточно места для их сохранения. Также, пароли включают в себя криптографическую соль (которая генерируется автоматически для каждого нового пароля), чтобы вам не нужно было с этим разбираться.

Хешировщик PBKDF2

Использование хешировщика PBKDF2 больше не рекомендуется с тех пор, как PHP добавил поддержку Sodium и BCrypt. Наследуемые приложения, все еще использующие его, рекомендуется обновить до более новых алгоритмов хеширования.

Создание пользовательского хешировщика паролей

Если вам нужно создать собственный, он должен следовать таким правилам:

  1. Класс должен реализовывать PasswordHasherInterface (вы также можете реализовать LegacyPasswordHasherInterface если ваш алгоритм хеша использует отдельную соль);
  2. Реализации hash() и verify() должны валидировать, что длина пароля не более 4096 знаков. Это из соображений безопасности (см. CVE-2013-5750).

    Вы можете использовать метод isPasswordTooLong() для такой проверки.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Security/Hasher/CustomVerySecureHasher.php
namespace App\Security\Hasher;

use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
use Symfony\Component\PasswordHasher\Hasher\CheckPasswordLengthTrait;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;

class CustomVerySecureHasher implements PasswordHasherInterface
{
    use CheckPasswordLengthTrait;

    public function hash(string $plainPassword): string
    {
        if ($this->isPasswordTooLong($plainPassword)) {
            throw new InvalidPasswordException();
        }

        // ... хешировать чистый пароль безопасным образом

        return $hashedPassword;
    }

    public function verify(string $hashedPassword, string $plainPassword): bool
    {
        if ('' === $plainPassword || $this->isPasswordTooLong($plainPassword)) {
            return false;
        }

        // ... валидировать, равняется ли пароль паролю пользователя, безопасным образом

        return $passwordIsValid;
    }
}

Теперь, определите хешировщик пароля, используя настройку id:

1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    # ...
    password_hashers:
        app_hasher:
            # сервис-ID вашего пользовательского хешировщика (FQCN, используя services.yaml по умолчанию)
            id: 'App\Security\Hasher\MyCustomPasswordHasher'