Аутентификация в Symfony: частые ошибки новичков
Настраиваете вход в Symfony 7 и получаете то бесконечный редирект, то «Invalid credentials»? Разбираем типичные грабли в security.yaml, хешировании паролей и работе аутентификаторов.

После make:auth и правки security.yaml вы открываете форму входа — и попадаете в цикл редиректов, ловите Invalid credentials при заведомо верном пароле или видите, что пользователь «залогинен», но isGranted('ROLE_USER') возвращает false. К концу материала вы будете понимать, где именно в Symfony 7.x ломается аутентификация у новичков и как это чинить. Разбор рассчитан на тех, кто уже поставил symfony/security-bundle и настраивает вход по логину и паролю.
Что должно быть до того, как что-то заработает
Проверьте, что у вас есть до начала — большинство ошибок растёт именно отсюда:
- Symfony 7.x (проверьте
composer show symfony/framework-bundle). Синтаксисsecurity.yamlниже — для нового аутентификатора, включённого по умолчанию с Symfony 5.4 и единственного в 6+. - Установлен
symfony/security-bundleи, если работаете с БД,symfony/orm-pack. - Сущность пользователя реализует
UserInterface, а для парольной аутентификации ещё иPasswordAuthenticatedUserInterface. - Настроен
DATABASE_URLи накачены миграции — таблица пользователей реально существует.
Ошибка 1. Пароль не хешируется — сравнивается plaintext с plaintext
Классика: пользователь создаётся напрямую, пароль пишется в поле password как есть, а на входе Symfony хеширует введённое и сравнивает с сырой строкой из базы. Итог — вечный Invalid credentials.
Пароль нужно хешировать тем же алгоритмом, что настроен в security.yaml. С Symfony автоматический выбор алгоритма делает auto:
# config/packages/security.yaml (Symfony 7.x)
security:
password_hashers:
App\Entity\User: 'auto'
Хешируйте через сервис UserPasswordHasherInterface, а не вручную:
<?php
// Пример: регистрация пользователя в контроллере (Symfony 7.x)
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
public function register(
UserPasswordHasherInterface $passwordHasher,
EntityManagerInterface $em
): void {
$user = new User();
$user->setEmail('user@example.com');
// Берём сырой пароль из формы и хешируем перед сохранением
$hashed = $passwordHasher->hashPassword($user, $plainPassword);
$user->setPassword($hashed);
$em->persist($user);
$em->flush();
}
Проверка: посмотрите в базе поле пароля. Если там читаемая строка — вы забыли хеширование. Должна быть длинная последовательность вида $2y$... или $argon2id$....
Про длину столбца
Хеш bcrypt занимает 60 символов, argon2id — заметно больше. Если поле password объявлено как VARCHAR(20), хеш обрежется молча, и вход перестанет работать. Команда make:user создаёт колонку достаточной длины, но при ручном изменении легко ошибиться.
Ошибка 2. getUserIdentifier возвращает не то поле
В новом слое безопасности провайдер ищет пользователя по «идентификатору», который отдаёт метод getUserIdentifier(). Новички оставляют там id или забывают синхронизировать с провайдером.
Если вы логинитесь по email, метод должен возвращать email, а провайдер — искать по тому же полю:
// config/packages/security.yaml
security:
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
<?php
// App\Entity\User
public function getUserIdentifier(): string
{
return (string) $this->email;
}
Проверка: если property в провайдере и поле в getUserIdentifier() расходятся, вы получите Username could not be found при верных данных.
Ошибка 3. Порядок firewalls и dev-фаервол
Symfony применяет первый подошедший firewall и дальше не смотрит. Если ваш основной firewall описан выше dev или его pattern перехватывает всё, включая профайлер, вы получите странные редиректы на профайлере и ассетах.
# config/packages/security.yaml
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
dev с security: false обязан идти первым. Иначе тулбар и профайлер начнут требовать авторизацию.
Firewall в Symfony — это не порядок в списке правил как в iptables. Срабатывает ровно один, самый первый подходящий по pattern, и на нём разбор заканчивается. Половина «мистических» редиректов новичка — это не тот firewall, что он думал.
Ошибка 4. access_control не проверяет то, что кажется
Правила access_control тоже проверяются сверху вниз до первого совпадения по path. Если общее правило стоит выше частного, частное никогда не сработает.
# Правильный порядок: частное выше общего
security:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }
Ещё частая беда — путь /login закрыт правилом roles: ROLE_USER. Тогда неавторизованного редиректит на /login, который сам требует авторизации — получаете бесконечный редирект. Страница входа должна быть доступна анонимно (обычно PUBLIC_ACCESS или отсутствие правила).
Ошибка 5. Роли без иерархии и без префикса ROLE_
Symfony считает ролью только строку, начинающуюся с ROLE_. Роль admin или editor без префикса просто игнорируется при проверке. И вторая частая ошибка: ROLE_ADMIN не наследует ROLE_USER автоматически.
# config/packages/security.yaml
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
Проверка: у пользователя с ролью ROLE_ADMIN вызовите isGranted('ROLE_USER'). Без иерархии вернётся false, и это ломает шаблоны, где вы разграничивали доступ.
getRoles должен всегда что-то возвращать
Метод getRoles() должен возвращать непустой массив. Если у пользователя нет ролей в базе, добавляйте ROLE_USER в коде, иначе аутентифицированный пользователь останется без единой роли:
<?php
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER'; // гарантируем базовую роль
return array_unique($roles);
}
Ошибка 6. Забытый logout или CSRF на форме входа
Выход настраивается в firewall, а не в контроллере. Без ключа logout маршрут app_logout не существует, и вы получите 404 или route not found.
# config/packages/security.yaml
security:
firewalls:
main:
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
Если enable_csrf: true, а в шаблоне формы нет скрытого поля с токеном, вход молча не проходит. Поле генерирует функция csrf_token('authenticate'):
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}">
Точное имя маркера CSRF и имена полей формы сверяйте с текущим разделом документации Security для вашей версии — детали form_login уточнялись между релизами.
Если не получилось
- Invalid credentials. Пароль в базе не хеширован либо хеширован другим алгоритмом; проверьте поле в БД и
password_hashers. - Username could not be found. Расходятся
propertyпровайдера иgetUserIdentifier(); либо пользователь ищется по несуществующему полю. - Бесконечный редирект на /login. Страница входа закрыта правилом
access_control; откройте её анонимно. - Профайлер требует авторизацию. Firewall
devстоит нижеmainили у него не выключенsecurity. - isGranted('ROLE_USER') = false у админа. Нет
role_hierarchyилиgetRoles()возвращает пустой массив.
Первым делом смотрите вкладку Security в профайлере (/_profiler): там видно, какой firewall сработал, кто текущий токен и какие роли назначены. Это снимает половину догадок.
Что делать дальше
Когда вход работает, настройте remember_me и проверьте инвалидацию сессии при смене пароля — это отдельная тема, здесь мы её не разворачивали. Для API вместо form_login смотрите в сторону токенов и отдельного аутентификатора. И заведите привычку открывать вкладку Security профайлера при любом странном поведении входа — она показывает реальное состояние, а не то, что вы предполагаете.
Карточки для маркетплейсов: Таблица товаров и папка фотографий на входе — комплект слайдов и описаний на выходе, по требованиям Wildberries и Ozon.
Спросить за 5 ₽Источники: Symfony — Security (официальная документация), Symfony — How to Build a Login Form
Частые вопросы
У меня Symfony 5.4, а не 7 — примеры подойдут?
Почему пароль верный, а я всё равно получаю Invalid credentials?
Как понять, какой firewall сработал на запросе?
Нужен ли мне ROLE_USER, если я явно назначаю ROLE_ADMIN?
Можно ли использовать эти настройки для аутентификации API?
Логин работает, но выход выдаёт 404 — что не так?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.